tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI-Logo

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5<a href="https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"><img src="https://github.com/Tim55667757/TKSBrokerAPI/blob/develop/docs/media/TKSBrokerAPI-Logo.png?raw=true" alt="TKSBrokerAPI-Logo" width="780" target="_blank" /></a>
   6
   7**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   8as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   9from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
  10
  11TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  12the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  13
  14- **Open account for trading:** https://tinkoff.ru/sl/AaX1Et1omnH
  15- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  16- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  17- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  18- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  19- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  20"""
  21
  22# Copyright (c) 2022 Gilmillin Timur Mansurovich
  23#
  24# Licensed under the Apache License, Version 2.0 (the "License");
  25# you may not use this file except in compliance with the License.
  26# You may obtain a copy of the License at
  27#
  28#     http://www.apache.org/licenses/LICENSE-2.0
  29#
  30# Unless required by applicable law or agreed to in writing, software
  31# distributed under the License is distributed on an "AS IS" BASIS,
  32# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  33# See the License for the specific language governing permissions and
  34# limitations under the License.
  35
  36
  37import sys
  38import os
  39from argparse import ArgumentParser
  40from importlib.metadata import version
  41
  42from dateutil.tz import tzlocal
  43from time import sleep
  44
  45import re
  46import json
  47import requests
  48import traceback as tb
  49
  50from multiprocessing import cpu_count, Lock
  51from multiprocessing.pool import ThreadPool
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self._tag = ""
 129        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 130
 131        self.__lock = Lock()  # initialize multiprocessing mutex lock
 132
 133        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
 134
 135        self.aliases = TKS_TICKER_ALIASES
 136        """Some aliases instead official tickers.
 137
 138        See also: `TKSEnums.TKS_TICKER_ALIASES`
 139        """
 140
 141        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 142
 143        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 144
 145        self._ticker = ""
 146        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 147
 148        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 149        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 150
 151        See also: `SearchByTicker()`, `SearchInstruments()`.
 152        """
 153
 154        self._figi = ""
 155        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 156
 157        See also: `SearchByFIGI()`, `SearchInstruments()`.
 158        """
 159
 160        self.depth = 1
 161        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 162
 163        See also: `GetCurrentPrices()`.
 164        """
 165
 166        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 167        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 168
 169        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 170        """
 171
 172        uLogger.debug("Broker API server: {}".format(self.server))
 173
 174        self.timeout = 15
 175        """Server operations timeout in seconds. Default: `15`.
 176
 177        See also: `SendAPIRequest()`.
 178        """
 179
 180        self.headers = {
 181            "Content-Type": "application/json",
 182            "accept": "application/json",
 183            "Authorization": "Bearer {}".format(self.token),
 184            "x-app-name": "Tim55667757.TKSBrokerAPI",
 185        }
 186        """
 187        Headers which send in every request to broker server. Please, do not change it!
 188        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
 189
 190        See also: `SendAPIRequest()`.
 191        """
 192
 193        self.body = None
 194        """Request body which send to broker server. Default: `None`.
 195
 196        See also: `SendAPIRequest()`.
 197        """
 198
 199        self.moreDebug = False
 200        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 201
 202        self.useHTMLReports = False
 203        """
 204        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 205        
 206        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 207        """
 208
 209        self.historyFile = None
 210        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 211
 212        See also: `History()`.
 213        """
 214
 215        self.htmlHistoryFile = "index.html"
 216        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 217
 218        See also: `ShowHistoryChart()`.
 219        """
 220
 221        self.instrumentsFile = "instruments.md"
 222        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 223
 224        See also: `ShowInstrumentsInfo()`.
 225        """
 226
 227        self.searchResultsFile = "search-results.md"
 228        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 229
 230        See also: `SearchInstruments()`.
 231        """
 232
 233        self.pricesFile = "prices.md"
 234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 235
 236        See also: `GetListOfPrices()`.
 237        """
 238
 239        self.infoFile = "info.md"
 240        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 241
 242        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 243        """
 244
 245        self.bondsXLSXFile = "ext-bonds.xlsx"
 246        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 247        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 248
 249        See also: `ExtendBondsData()`.
 250        """
 251
 252        self.calendarFile = "calendar.md"
 253        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 254        
 255        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 256
 257        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 258        """
 259
 260        self.overviewFile = "overview.md"
 261        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 262
 263        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 264        """
 265
 266        self.overviewDigestFile = "overview-digest.md"
 267        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 268
 269        See also: `Overview()` with parameter `details="digest"`.
 270        """
 271
 272        self.overviewPositionsFile = "overview-positions.md"
 273        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 274
 275        See also: `Overview()` with parameter `details="positions"`.
 276        """
 277
 278        self.overviewOrdersFile = "overview-orders.md"
 279        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 280
 281        See also: `Overview()` with parameter `details="orders"`.
 282        """
 283
 284        self.overviewAnalyticsFile = "overview-analytics.md"
 285        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 286
 287        See also: `Overview()` with parameter `details="analytics"`.
 288        """
 289
 290        self.overviewBondsCalendarFile = "overview-calendar.md"
 291        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 292
 293        See also: `Overview()` with parameter `details="calendar"`.
 294        """
 295
 296        self.reportFile = "deals.md"
 297        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 298
 299        See also: `Deals()`.
 300        """
 301
 302        self.withdrawalLimitsFile = "limits.md"
 303        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 304
 305        See also: `OverviewLimits()` and `RequestLimits()`.
 306        """
 307
 308        self.userInfoFile = "user-info.md"
 309        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 310
 311        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 312        """
 313
 314        self.userAccountsFile = "accounts.md"
 315        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 316
 317        See also: `OverviewAccounts()`, `RequestAccounts()`.
 318        """
 319
 320        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 321        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 322
 323        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 324
 325        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 326        """
 327
 328        self.iList = None  # init iList for raw instruments data
 329        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 330        
 331        See also: `Listing()`, `DumpInstruments()`.
 332        """
 333
 334        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 335        if useCache:
 336            if os.path.exists(self.iListDumpFile):
 337                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 338                curTime = datetime.now(tzutc())
 339
 340                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 341                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 342
 343                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 344
 345                else:
 346                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 347
 348                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 349                        os.path.abspath(self.iListDumpFile),
 350                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 351                    ))
 352
 353            else:
 354                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 355                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 356
 357        else:
 358            self.iList = self.Listing()  # request new raw instruments data from broker server
 359            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 360
 361        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 362        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 363
 364        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 365        """
 366
 367    @property
 368    def tag(self) -> str:
 369        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 370        return self._tag
 371
 372    @tag.setter
 373    def tag(self, value):
 374        """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 375        self._tag = str(value)
 376
 377        if self._tag:
 378            for handler in uLogger.handlers:
 379                handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag)))
 380
 381            uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag))
 382
 383        else:
 384            for handler in uLogger.handlers:
 385                handler.setFormatter(uLog.logging.Formatter(uLog.formatString))
 386
 387            uLogger.debug("Default logger format is used")
 388
 389    @property
 390    def ticker(self) -> str:
 391        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 392
 393        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 394        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 395
 396        See also: `SearchByTicker()`, `SearchInstruments()`.
 397        """
 398        return self._ticker
 399
 400    @ticker.setter
 401    def ticker(self, value):
 402        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 403
 404        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 405        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 406
 407        See also: `SearchByTicker()`, `SearchInstruments()`.
 408        """
 409        self._ticker = str(value).upper()  # Tickers may be upper case only
 410
 411    @property
 412    def figi(self) -> str:
 413        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 414
 415        See also: `SearchByFIGI()`, `SearchInstruments()`.
 416        """
 417        return self._figi
 418
 419    @figi.setter
 420    def figi(self, value):
 421        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 422
 423        See also: `SearchByFIGI()`, `SearchInstruments()`.
 424        """
 425        self._figi = str(value).upper()  # FIGI may be upper case only
 426
 427    def _ParseJSON(self, rawData="{}") -> dict:
 428        """
 429        Parse JSON from response string.
 430
 431        :param rawData: this is a string with JSON-formatted text.
 432        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 433        """
 434        try:
 435            responseJSON = json.loads(rawData) if rawData else {}
 436
 437            if self.moreDebug:
 438                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 439
 440            return responseJSON
 441
 442        except Exception as e:
 443            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 444
 445            return {}
 446
 447    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 448        """
 449        Send GET or POST request to broker server and receive JSON object.
 450
 451        self.header: must be defining with dictionary of headers.
 452        self.body: if define then used as request body. None by default.
 453        self.timeout: global request timeout, 15 seconds by default.
 454        :param url: url with REST request.
 455        :param reqType: send "GET" or "POST" request. "GET" by default.
 456        :param retry: how many times retry after first request if an 5xx server errors occurred.
 457        :param pause: sleep time in seconds between retries.
 458        :return: response JSON (dictionary) from broker.
 459        """
 460        if reqType.upper() not in ("GET", "POST"):
 461            uLogger.error("You can define request type: `GET` or `POST`!")
 462            raise Exception("Incorrect value")
 463
 464        if self.moreDebug:
 465            uLogger.debug("Request parameters:")
 466            uLogger.debug("    - REST API URL: {}".format(url))
 467            uLogger.debug("    - request type: {}".format(reqType))
 468            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 469            uLogger.debug("    - body:\n{}".format(self.body))
 470
 471        # fast hack to avoid all operations with some tickers/FIGI
 472        responseJSON = {}
 473        oK = True
 474        for item in self.exclude:
 475            if item in url:
 476                if self.moreDebug:
 477                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 478
 479                oK = False
 480                break
 481
 482        if oK:
 483            with self.__lock:  # acquire the mutex lock
 484                counter = 0
 485                response = None
 486                errMsg = ""
 487
 488                while not response and counter <= retry:
 489                    if reqType == "GET":
 490                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 491
 492                    if reqType == "POST":
 493                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 494
 495                    if self.moreDebug:
 496                        uLogger.debug("Response:")
 497                        uLogger.debug("    - status code: {}".format(response.status_code))
 498                        uLogger.debug("    - reason: {}".format(response.reason))
 499                        uLogger.debug("    - body length: {}".format(len(response.text)))
 500                        uLogger.debug("    - headers:\n{}".format(response.headers))
 501
 502                    # Server returns some headers:
 503                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 504                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 505                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 506                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 507                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 508                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 509                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 510                        sleep(rateLimitWait)
 511
 512                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 513                    if 400 <= response.status_code < 500:
 514                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 515                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 516
 517                        if "code" in response.text and "message" in response.text:
 518                            msgDict = self._ParseJSON(rawData=response.text)
 519                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 520
 521                        counter = retry + 1  # do not retry for 4xx errors
 522
 523                    if 500 <= response.status_code < 600:
 524                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 525                        uLogger.debug("    - not oK, {}".format(errMsg))
 526
 527                        if "code" in response.text and "message" in response.text:
 528                            errMsgDict = self._ParseJSON(rawData=response.text)
 529                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 530
 531                        counter += 1
 532
 533                        if counter <= retry:
 534                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 535                            sleep(pause)
 536
 537                responseJSON = self._ParseJSON(rawData=response.text)
 538
 539                if errMsg:
 540                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 541                    uLogger.error("    - not oK, {}".format(errMsg))
 542
 543        return responseJSON
 544
 545    def _IUpdater(self, iType: str) -> tuple:
 546        """
 547        Request instrument by type from server. See available API methods for instruments:
 548        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 549        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 550        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 551        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 552        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 553
 554        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 555        :return: tuple with iType name and list of available instruments of current type for defined user token.
 556        """
 557        result = []
 558
 559        if iType in TKS_INSTRUMENTS:
 560            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 561
 562            # all instruments have the same body in API v2 requests:
 563            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 564            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 565            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 566
 567        return iType, result
 568
 569    def _IWrapper(self, kwargs):
 570        """
 571        Wrapper runs instrument's update method `_IUpdater()`.
 572        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 573        """
 574        return self._IUpdater(**kwargs)
 575
 576    def Listing(self) -> dict:
 577        """
 578        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 579
 580        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 581        """
 582        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 583        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 584
 585        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 586        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 587        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 588
 589        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 590        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 591        poolUpdater.close()  # close the thread pool
 592        poolUpdater.join()  # wait a moment until all data returns from threads
 593
 594        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 595        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 596        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 597
 598        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 599        for iType in iList.keys():
 600            for ticker in iList[iType]:
 601                iList[iType][ticker]["type"] = iType
 602
 603                if "minPriceIncrement" in iList[iType][ticker].keys():
 604                    iList[iType][ticker]["step"] = NanoToFloat(
 605                        iList[iType][ticker]["minPriceIncrement"]["units"],
 606                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 607                    )
 608
 609                else:
 610                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 611
 612        return iList
 613
 614    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 615        """
 616        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 617
 618        See also: `DumpInstruments()`, `Listing()`.
 619
 620        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 621                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 622        """
 623        if self.iListDumpFile is None or not self.iListDumpFile:
 624            uLogger.error("Output name of dump file must be defined!")
 625            raise Exception("Filename required")
 626
 627        if not self.iList or forceUpdate:
 628            self.iList = self.Listing()
 629
 630        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 631
 632        # Save as XLSX with separated sheets for every type of instruments:
 633        with pd.ExcelWriter(
 634                path=xlsxDumpFile,
 635                date_format=TKS_DATE_FORMAT,
 636                datetime_format=TKS_DATE_TIME_FORMAT,
 637                mode="w",
 638        ) as writer:
 639            for iType in TKS_INSTRUMENTS:
 640                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 641                df = df[sorted(df)]  # sorted by column names
 642                df = df.applymap(
 643                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 644                    na_action="ignore",
 645                )  # converting numbers from nano-type to float in every cell
 646                df.to_excel(
 647                    writer,
 648                    sheet_name=iType,
 649                    encoding="UTF-8",
 650                    freeze_panes=(1, 1),
 651                )  # saving as XLSX-file with freeze first row and column as headers
 652
 653        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 654
 655    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 656        """
 657        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 658        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 659
 660        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 661
 662        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 663                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 664        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 665        """
 666        if self.iListDumpFile is None or not self.iListDumpFile:
 667            uLogger.error("Output name of dump file must be defined!")
 668            raise Exception("Filename required")
 669
 670        if not self.iList or forceUpdate:
 671            self.iList = self.Listing()
 672
 673        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 674        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 675            fH.write(jsonDump)
 676
 677        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 678
 679        return jsonDump
 680
 681    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 682        """
 683        Show information about one instrument defined by json data and prints it in Markdown format.
 684
 685        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 686
 687        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 688        :param show: if `True` then also printing information about instrument and its current price.
 689        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 690        :return: multilines text in Markdown format with information about one instrument.
 691        """
 692        splitLine = "|                                                             |                                                        |\n"
 693        infoText = ""
 694
 695        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 696            info = [
 697                "# Main information\n\n",
 698                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 699                "| Parameters                                                  | Values                                                 |\n",
 700                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 701                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 702                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 703            ]
 704
 705            if "sector" in iJSON.keys() and iJSON["sector"]:
 706                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 707
 708            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 709                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 710
 711            info.extend([
 712                splitLine,
 713                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 714                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 715            ])
 716
 717            if "isin" in iJSON.keys() and iJSON["isin"]:
 718                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 719
 720            if "classCode" in iJSON.keys():
 721                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 722
 723            info.extend([
 724                splitLine,
 725                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 726                splitLine,
 727                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 728                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 729                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 730            ])
 731
 732            if iJSON["figi"]:
 733                self._figi = iJSON["figi"]
 734                iJSON = iJSON | self.RequestTradingStatus()
 735
 736                info.extend([
 737                    splitLine,
 738                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 739                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 740                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 741                ])
 742
 743            info.append(splitLine)
 744
 745            if "type" in iJSON.keys() and iJSON["type"]:
 746                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 747
 748                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 749                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 750
 751            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 752                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 753
 754            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 755                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 756
 757            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 758                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 759
 760            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 761                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 762
 763            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 764                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 765
 766            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 767                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 768
 769            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 770                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 771
 772            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 773                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 774
 775            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 776                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 777
 778            if "currency" in iJSON.keys():
 779                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 780
 781            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 782                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 783
 784            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 785                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 786
 787            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 788                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 789
 790            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 791                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 792
 793            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 794                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 795
 796            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 797                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 798
 799            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 800                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 801
 802            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 803                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 804
 805            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 806                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 807
 808            iExt = None
 809            if iJSON["type"] == "Bonds":
 810                info.extend([
 811                    splitLine,
 812                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 813                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 814                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 815                        iJSON["nominal"]["currency"],
 816                    )),
 817                ])
 818
 819                if "floatingCouponFlag" in iJSON.keys():
 820                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 821
 822                if "amortizationFlag" in iJSON.keys():
 823                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 824
 825                info.append(splitLine)
 826
 827                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 828                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 829
 830                if iJSON["figi"]:
 831                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 832
 833                    info.extend([
 834                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 835                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 836                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 837                    ])
 838
 839                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 840                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 841                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 842                        iJSON["aciValue"]["currency"]
 843                    )))
 844
 845            if "currentPrice" in iJSON.keys():
 846                info.append(splitLine)
 847
 848                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 849                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 850
 851                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 852                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 853                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 854                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 855                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 856
 857                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 858                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 859
 860                info.extend([
 861                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 862                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 863                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 864                    )),
 865                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 866                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 867                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 868                    )),
 869                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 870                        "{:.2f}%{}".format(
 871                            iJSON["currentPrice"]["changes"],
 872                            " ({}{:.2f} {})".format(
 873                                "+" if bondChangesDelta > 0 else "",
 874                                bondChangesDelta,
 875                                aciCurrency
 876                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 877                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 878                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 879                                currency
 880                            ),
 881                        )
 882                    ),
 883                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 884                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 885                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 886                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 887                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 888                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 889                    )),
 890                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 891                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 892                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 893                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 894                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 895                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 896                    )),
 897                ])
 898
 899            if "lot" in iJSON.keys():
 900                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 901
 902            if "step" in iJSON.keys() and iJSON["step"] != 0:
 903                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 904
 905            # Add bond payment calendar:
 906            if iJSON["type"] == "Bonds":
 907                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 908                info.extend(["\n#", strCalendar])
 909
 910            infoText += "".join(info)
 911
 912            if show and not onlyFiles:
 913                uLogger.info("{}".format(infoText))
 914
 915            if self.infoFile is not None and (show or onlyFiles):
 916                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 917                    fH.write(infoText)
 918
 919                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 920
 921                if self.useHTMLReports:
 922                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 923                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 924                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 925
 926                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 927
 928        return infoText
 929
 930    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 931        """
 932        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 933
 934        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 935        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 936        :return: JSON formatted data with information about instrument.
 937        """
 938        tickerJSON = {}
 939        if self.moreDebug:
 940            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 941
 942        if not self._ticker:
 943            uLogger.warning("self._ticker variable is not be empty!")
 944
 945        else:
 946            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 947                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 948                raise Exception("Instrument not allowed")
 949
 950            if not self.iList:
 951                self.iList = self.Listing()
 952
 953            if self._ticker in self.iList["Shares"].keys():
 954                tickerJSON = self.iList["Shares"][self._ticker]
 955                if self.moreDebug:
 956                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 957
 958            elif self._ticker in self.iList["Currencies"].keys():
 959                tickerJSON = self.iList["Currencies"][self._ticker]
 960                if self.moreDebug:
 961                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 962
 963            elif self._ticker in self.iList["Bonds"].keys():
 964                tickerJSON = self.iList["Bonds"][self._ticker]
 965                if self.moreDebug:
 966                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 967
 968            elif self._ticker in self.iList["Etfs"].keys():
 969                tickerJSON = self.iList["Etfs"][self._ticker]
 970                if self.moreDebug:
 971                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 972
 973            elif self._ticker in self.iList["Futures"].keys():
 974                tickerJSON = self.iList["Futures"][self._ticker]
 975                if self.moreDebug:
 976                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 977
 978        if tickerJSON:
 979            self._figi = tickerJSON["figi"]
 980
 981            if requestPrice:
 982                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 983
 984                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 985                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 986
 987                else:
 988                    tickerJSON["currentPrice"]["changes"] = 0
 989
 990            if show:
 991                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 992
 993        else:
 994            if show:
 995                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 996
 997        return tickerJSON
 998
 999    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1000        """
1001        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1002
1003        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1004        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1005        :return: JSON formatted data with information about instrument.
1006        """
1007        figiJSON = {}
1008        if self.moreDebug:
1009            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1010
1011        if not self._figi:
1012            uLogger.warning("self._figi variable is not be empty!")
1013
1014        else:
1015            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1016                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1017                raise Exception("Instrument not allowed")
1018
1019            if not self.iList:
1020                self.iList = self.Listing()
1021
1022            for item in self.iList["Shares"].keys():
1023                if self._figi == self.iList["Shares"][item]["figi"]:
1024                    figiJSON = self.iList["Shares"][item]
1025
1026                    if self.moreDebug:
1027                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1028
1029                    break
1030
1031            if not figiJSON:
1032                for item in self.iList["Currencies"].keys():
1033                    if self._figi == self.iList["Currencies"][item]["figi"]:
1034                        figiJSON = self.iList["Currencies"][item]
1035
1036                        if self.moreDebug:
1037                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1038
1039                        break
1040
1041            if not figiJSON:
1042                for item in self.iList["Bonds"].keys():
1043                    if self._figi == self.iList["Bonds"][item]["figi"]:
1044                        figiJSON = self.iList["Bonds"][item]
1045
1046                        if self.moreDebug:
1047                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1048
1049                        break
1050
1051            if not figiJSON:
1052                for item in self.iList["Etfs"].keys():
1053                    if self._figi == self.iList["Etfs"][item]["figi"]:
1054                        figiJSON = self.iList["Etfs"][item]
1055
1056                        if self.moreDebug:
1057                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1058
1059                        break
1060
1061            if not figiJSON:
1062                for item in self.iList["Futures"].keys():
1063                    if self._figi == self.iList["Futures"][item]["figi"]:
1064                        figiJSON = self.iList["Futures"][item]
1065
1066                        if self.moreDebug:
1067                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1068
1069                        break
1070
1071        if figiJSON:
1072            self._figi = figiJSON["figi"]
1073            self._ticker = figiJSON["ticker"]
1074
1075            if requestPrice:
1076                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1077
1078                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1079                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1080
1081                else:
1082                    figiJSON["currentPrice"]["changes"] = 0
1083
1084            if show:
1085                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1086
1087        else:
1088            if show:
1089                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1090
1091        return figiJSON
1092
1093    def GetCurrentPrices(self, show: bool = True) -> dict:
1094        """
1095        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1096        `{"buy": [{"price": 1243.8, "quantity": 193},
1097                  {"price": 1244.0, "quantity": 168},
1098                  {"price": 1244.8, "quantity": 5},
1099                  {"price": 1245.0, "quantity": 61},
1100                  {"price": 1245.4, "quantity": 60}],
1101          "sell": [{"price": 1243.6, "quantity": 8},
1102                   {"price": 1242.6, "quantity": 10},
1103                   {"price": 1242.4, "quantity": 18},
1104                   {"price": 1242.2, "quantity": 50},
1105                   {"price": 1242.0, "quantity": 113}],
1106          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1107        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1108        - sell: list of dicts with Buyers prices,
1109            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1110            - quantity: volume value by current price in lots,
1111        - limitUp: current trade session limit price, maximum,
1112        - limitDown: current trade session limit price, minimum,
1113        - lastPrice: last deal price of the instrument,
1114        - closePrice: previous trade session close price of the instrument.
1115
1116        See also: `SearchByTicker()` and `SearchByFIGI()`.
1117        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1118        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1119
1120        :param show: if `True` then print DOM to log and console.
1121        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1122                 If an error occurred then returns an empty record:
1123                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1124        """
1125        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1126
1127        if self.depth < 1:
1128            uLogger.error("Depth of Market (DOM) must be >=1!")
1129            raise Exception("Incorrect value")
1130
1131        if not (self._ticker or self._figi):
1132            uLogger.error("self._ticker or self._figi variables must be defined!")
1133            raise Exception("Ticker or FIGI required")
1134
1135        if self._ticker and not self._figi:
1136            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1137            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1138
1139        if not self._ticker and self._figi:
1140            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1141            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1142
1143        if not self._figi:
1144            uLogger.error("FIGI is not defined!")
1145            raise Exception("Ticker or FIGI required")
1146
1147        else:
1148            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1149
1150            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1151            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1152            self.body = str({"figi": self._figi, "depth": self.depth})
1153            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1154
1155            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1156                # list of dicts with sellers orders:
1157                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1158
1159                # list of dicts with buyers orders:
1160                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1161
1162                # max price of instrument at this time:
1163                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1164
1165                # min price of instrument at this time:
1166                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1167
1168                # last price of deal with instrument:
1169                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1170
1171                # last close price of instrument:
1172                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1173
1174            else:
1175                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1176                uLogger.debug("Server response: {}".format(pricesResponse))
1177
1178            if show:
1179                if prices["buy"] or prices["sell"]:
1180                    info = [
1181                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1182                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1183                            self._ticker,
1184                            self._figi,
1185                            self.depth,
1186                        ),
1187                        "-" * 60, "\n",
1188                        "             Orders of Buyers | Orders of Sellers\n",
1189                        "-" * 60, "\n",
1190                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1191                        "-" * 60, "\n",
1192                    ]
1193
1194                    if not prices["buy"]:
1195                        info.append("                              | No orders!\n")
1196                        sumBuy = 0
1197
1198                    else:
1199                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1200                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1201                        for item in maxMinSorted:
1202                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1203
1204                    if not prices["sell"]:
1205                        info.append("No orders!                    |\n")
1206                        sumSell = 0
1207
1208                    else:
1209                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1210                        for item in prices["sell"]:
1211                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1212
1213                    info.extend([
1214                        "-" * 60, "\n",
1215                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1216                        "-" * 60, "\n",
1217                    ])
1218
1219                    infoText = "".join(info)
1220
1221                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1222
1223                else:
1224                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1225
1226        return prices
1227
1228    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1229        """
1230        This method get and show information about all available broker instruments for current user account.
1231        If `instrumentsFile` string is not empty then also save information to this file.
1232
1233        :param show: if `True` then print results to console, if `False` — print only to file.
1234        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1235        :return: multi-lines string with all available broker instruments.
1236        """
1237        if not self.iList:
1238            self.iList = self.Listing()
1239
1240        info = [
1241            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1242            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1243        ]
1244
1245        # add instruments count by type:
1246        for iType in self.iList.keys():
1247            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1248
1249        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1250        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1251
1252        # generating info tables with all instruments by type:
1253        for iType in self.iList.keys():
1254            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1255
1256            for instrument in self.iList[iType].keys():
1257                iName = self.iList[iType][instrument]["name"]  # instrument's name
1258                if len(iName) > 57:
1259                    iName = "{}...".format(iName[:54])  # right trim for a long string
1260
1261                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1262                    self.iList[iType][instrument]["ticker"],
1263                    iName,
1264                    self.iList[iType][instrument]["figi"],
1265                    self.iList[iType][instrument]["currency"],
1266                    self.iList[iType][instrument]["lot"],
1267                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1268                ))
1269
1270        infoText = "".join(info)
1271
1272        if show and not onlyFiles:
1273            uLogger.info(infoText)
1274
1275        if self.instrumentsFile and (show or onlyFiles):
1276            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1277                fH.write(infoText)
1278
1279            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1280
1281            if self.useHTMLReports:
1282                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1283                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1284                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1285
1286                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1287
1288        return infoText
1289
1290    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1291        """
1292        This method search and show information about instruments by part of its ticker, FIGI or name.
1293        If `searchResultsFile` string is not empty then also save information to this file.
1294
1295        :param pattern: string with part of ticker, FIGI or instrument's name.
1296        :param show: if `True` then print results to console, if `False` — return list of result only.
1297        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1298        :return: list of dictionaries with all found instruments.
1299        """
1300        if not self.iList:
1301            self.iList = self.Listing()
1302
1303        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1304        compiledPattern = re.compile(pattern, re.IGNORECASE)
1305
1306        for iType in self.iList:
1307            for instrument in self.iList[iType].values():
1308                searchResult = compiledPattern.search(" ".join(
1309                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1310                ))
1311
1312                if searchResult:
1313                    searchResults[iType][instrument["ticker"]] = instrument
1314
1315        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1316        info = [
1317            "# Search results\n\n",
1318            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1319            "* **Search pattern:** [{}]\n".format(pattern),
1320            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1321            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1322        ]
1323        infoShort = info[:]
1324
1325        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1326        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1327        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1328
1329        if resultsLen == 0:
1330            info.append("\nNo results\n")
1331            infoShort.append("\nNo results\n")
1332            uLogger.warning("No results. Try changing your search pattern.")
1333
1334        else:
1335            for iType in searchResults:
1336                iTypeValuesCount = len(searchResults[iType].values())
1337                if iTypeValuesCount > 0:
1338                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1339                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1340
1341                    for instrument in searchResults[iType].values():
1342                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1343                            instrument["type"],
1344                            instrument["ticker"],
1345                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1346                            instrument["figi"],
1347                        ))
1348
1349                    if iTypeValuesCount <= 5:
1350                        infoShort.extend(info[-iTypeValuesCount:])
1351
1352                    else:
1353                        infoShort.extend(info[-5:])
1354                        infoShort.append(skippedLine)
1355
1356        infoText = "".join(info)
1357        infoTextShort = "".join(infoShort)
1358
1359        if show and not onlyFiles:
1360            uLogger.info(infoTextShort)
1361            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1362
1363        if self.searchResultsFile and (show or onlyFiles):
1364            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1365                fH.write(infoText)
1366
1367            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1368
1369            if self.useHTMLReports:
1370                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1371                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1372                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1373
1374                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1375
1376        return searchResults
1377
1378    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1379        """
1380        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1381
1382        :param instruments: list of strings with tickers or FIGIs.
1383        :return: list with unique instrument FIGIs only.
1384        """
1385        requestedInstruments = []
1386        for iName in instruments:
1387            if iName not in self.aliases.keys():
1388                if iName not in requestedInstruments:
1389                    requestedInstruments.append(iName)
1390
1391            else:
1392                if iName not in requestedInstruments:
1393                    if self.aliases[iName] not in requestedInstruments:
1394                        requestedInstruments.append(self.aliases[iName])
1395
1396        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1397
1398        onlyUniqueFIGIs = []
1399        for iName in requestedInstruments:
1400            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1401                continue
1402
1403            self._ticker = iName
1404            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1405
1406            if not iData:
1407                self._ticker = ""
1408                self._figi = iName
1409
1410                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1411
1412                if not iData:
1413                    self._figi = ""
1414                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1415
1416            if iData and iData["figi"] not in onlyUniqueFIGIs:
1417                onlyUniqueFIGIs.append(iData["figi"])
1418
1419        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1420
1421        return onlyUniqueFIGIs
1422
1423    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1424        """
1425        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1426
1427        See limits: https://tinkoff.github.io/investAPI/limits/
1428
1429        If `pricesFile` string is not empty then also save information to this file.
1430
1431        :param instruments: list of strings with tickers or FIGIs.
1432        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1433        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1434        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1435                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1436        """
1437        if instruments is None or not instruments:
1438            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1439            raise Exception("Ticker or FIGI required")
1440
1441        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1442
1443        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1444
1445        iList = []  # trying to get info and current prices about all unique instruments:
1446        for self._figi in onlyUniqueFIGIs:
1447            iData = self.SearchByFIGI(requestPrice=True, show=False)
1448            iList.append(iData)
1449
1450        self.ShowListOfPrices(iList, show, onlyFiles)
1451
1452        return iList
1453
1454    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1455        """
1456        Show table contains current prices of given instruments.
1457
1458        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1459                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1460        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1461        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1462        :return: multilines text in Markdown format as a table contains current prices.
1463        """
1464        infoText = ""
1465
1466        if show or self.pricesFile or onlyFiles:
1467            info = [
1468                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1469                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1470                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1471            ]
1472
1473            for item in iList:
1474                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1475                    item["ticker"],
1476                    item["figi"],
1477                    item["type"],
1478                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1479                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1480                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1481                    "{} / {}".format(
1482                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1483                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1484                    ),
1485                    "{} / {}".format(
1486                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1487                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1488                    ),
1489                    item["currency"],
1490                ))
1491
1492            infoText = "".join(info)
1493
1494            if show and not onlyFiles:
1495                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1496
1497            if self.pricesFile and (show or onlyFiles):
1498                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1499                    fH.write(infoText)
1500
1501                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1502
1503                if self.useHTMLReports:
1504                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1505                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1506                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1507
1508                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1509
1510        return infoText
1511
1512    def RequestTradingStatus(self) -> dict:
1513        """
1514        Requesting trading status for the instrument defined by `figi` variable.
1515
1516        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1517
1518        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1519
1520        :return: dictionary with trading status attributes. Response example:
1521                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1522                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1523        """
1524        if self._figi is None or not self._figi:
1525            uLogger.error("Variable `figi` must be defined for using this method!")
1526            raise Exception("FIGI required")
1527
1528        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1529
1530        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1531        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1532        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1533
1534        if self.moreDebug:
1535            uLogger.debug("Records about current trading status successfully received")
1536
1537        return tradingStatus
1538
1539    def RequestPortfolio(self) -> dict:
1540        """
1541        Requesting actual user's portfolio for current `accountId`.
1542
1543        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1544
1545        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1546
1547        :return: dictionary with user's portfolio.
1548        """
1549        if self.accountId is None or not self.accountId:
1550            uLogger.error("Variable `accountId` must be defined for using this method!")
1551            raise Exception("Account ID required")
1552
1553        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1554
1555        self.body = str({"accountId": self.accountId})
1556        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1557        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1558
1559        if self.moreDebug:
1560            uLogger.debug("Records about user's portfolio successfully received")
1561
1562        return rawPortfolio
1563
1564    def RequestPositions(self) -> dict:
1565        """
1566        Requesting open positions by currencies and instruments for current `accountId`.
1567
1568        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1569
1570        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1571
1572        :return: dictionary with open positions by instruments.
1573        """
1574        if self.accountId is None or not self.accountId:
1575            uLogger.error("Variable `accountId` must be defined for using this method!")
1576            raise Exception("Account ID required")
1577
1578        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1579
1580        self.body = str({"accountId": self.accountId})
1581        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1582        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1583
1584        if self.moreDebug:
1585            uLogger.debug("Records about current open positions successfully received")
1586
1587        return rawPositions
1588
1589    def RequestPendingOrders(self) -> list:
1590        """
1591        Requesting current actual pending limit orders for current `accountId`.
1592
1593        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1594
1595        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1596
1597        :return: list of dictionaries with pending limit orders.
1598        """
1599        if self.accountId is None or not self.accountId:
1600            uLogger.error("Variable `accountId` must be defined for using this method!")
1601            raise Exception("Account ID required")
1602
1603        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1604
1605        self.body = str({"accountId": self.accountId})
1606        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1607        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1608
1609        if "orders" in rawResponse.keys():
1610            rawOrders = rawResponse["orders"]
1611            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1612
1613        else:
1614            rawOrders = []
1615            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1616
1617        return rawOrders
1618
1619    def RequestStopOrders(self) -> list:
1620        """
1621        Requesting current actual stop orders for current `accountId`.
1622
1623        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1624
1625        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1626
1627        :return: list of dictionaries with stop orders.
1628        """
1629        if self.accountId is None or not self.accountId:
1630            uLogger.error("Variable `accountId` must be defined for using this method!")
1631            raise Exception("Account ID required")
1632
1633        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1634
1635        self.body = str({"accountId": self.accountId})
1636        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1637        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1638
1639        if "stopOrders" in rawResponse.keys():
1640            rawStopOrders = rawResponse["stopOrders"]
1641            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1642
1643        else:
1644            rawStopOrders = []
1645            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1646
1647        return rawStopOrders
1648
1649    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1650        """
1651        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1652        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1653        and `overviewBondsCalendarFile` are defined then also save information to file.
1654
1655        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1656        many requests about the state of the portfolio, and then, based on the received data, a large number
1657        of calculation and statistics are collected.
1658
1659        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1660        :param details: how detailed should the information be?
1661        - `full` — shows full available information about portfolio status (by default),
1662        - `positions` — shows only open positions,
1663        - `orders` — shows only sections of open limits and stop orders.
1664        - `digest` — show a short digest of the portfolio status,
1665        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1666        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1667        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1668        :return: dictionary with client's raw portfolio and some statistics.
1669        """
1670        if self.accountId is None or not self.accountId:
1671            uLogger.error("Variable `accountId` must be defined for using this method!")
1672            raise Exception("Account ID required")
1673
1674        view = {
1675            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1676                "headers": {},  # list of dictionaries, response headers without "positions" section
1677                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1678                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1679                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1680                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1681                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1682                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1683                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1684                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1685                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1686            },
1687            "stat": {  # --- some statistics calculated using "raw" sections:
1688                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1689                "availableRUB": 0.,  # available rubles (without other currencies)
1690                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1691                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1692                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1693                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1694                "sharesCostRUB": 0.,  # costs of all shares in RUB
1695                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1696                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1697                "futuresCostRUB": 0.,  # costs of all futures in RUB
1698                "Currencies": [],  # list of dictionaries of all currencies statistics
1699                "Shares": [],  # list of dictionaries of all shares statistics
1700                "Bonds": [],  # list of dictionaries of all bonds statistics
1701                "Etfs": [],  # list of dictionaries of all etfs statistics
1702                "Futures": [],  # list of dictionaries of all futures statistics
1703                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1704                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1705                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1706                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1707                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1708            },
1709            "analytics": {  # --- some analytics of portfolio:
1710                "distrByAssets": {},  # portfolio distribution by assets
1711                "distrByCompanies": {},  # portfolio distribution by companies
1712                "distrBySectors": {},  # portfolio distribution by sectors
1713                "distrByCurrencies": {},  # portfolio distribution by currencies
1714                "distrByCountries": {},  # portfolio distribution by countries
1715                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1716            }
1717        }
1718
1719        details = details.lower()
1720        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1721        if details not in availableDetails:
1722            details = "full"
1723            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1724
1725        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1726
1727        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1728        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1729        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1730        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1731
1732        # save response headers without "positions" section:
1733        for key in portfolioResponse.keys():
1734            if key != "positions":
1735                view["raw"]["headers"][key] = portfolioResponse[key]
1736
1737            else:
1738                continue
1739
1740        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1741        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1742        for item in portfolioResponse["positions"]:
1743            if item["instrumentType"] == "currency":
1744                self._figi = item["figi"]
1745                if not self._figi and item["ticker"]:
1746                    self._ticker = item["ticker"]
1747                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1748
1749                curr = self.SearchByFIGI(requestPrice=False)
1750
1751                # current price of currency in RUB:
1752                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1753                    "name": curr["name"],
1754                    "currentPrice": NanoToFloat(
1755                        item["currentPrice"]["units"],
1756                        item["currentPrice"]["nano"]
1757                    ),
1758                }
1759
1760                view["raw"]["Currencies"].append(item)
1761
1762            elif item["instrumentType"] == "share":
1763                view["raw"]["Shares"].append(item)
1764
1765            elif item["instrumentType"] == "bond":
1766                view["raw"]["Bonds"].append(item)
1767
1768            elif item["instrumentType"] == "etf":
1769                view["raw"]["Etfs"].append(item)
1770
1771            elif item["instrumentType"] == "futures":
1772                view["raw"]["Futures"].append(item)
1773
1774            else:
1775                continue
1776
1777        # how many volume of currencies (by ISO currency name) are blocked:
1778        for item in view["raw"]["positions"]["blocked"]:
1779            blocked = NanoToFloat(item["units"], item["nano"])
1780            if blocked > 0:
1781                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1782
1783        # how many volume of instruments (by FIGI) are blocked:
1784        for item in view["raw"]["positions"]["securities"]:
1785            blocked = int(item["blocked"])
1786            if blocked > 0:
1787                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1788
1789        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1790
1791        if "rub" in allBlocked.keys():
1792            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1793
1794        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1795        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1796        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1797        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1798        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1799        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1800        view["stat"]["portfolioCostRUB"] = sum([
1801            view["stat"]["allCurrenciesCostRUB"],
1802            view["stat"]["sharesCostRUB"],
1803            view["stat"]["bondsCostRUB"],
1804            view["stat"]["etfsCostRUB"],
1805            view["stat"]["futuresCostRUB"],
1806        ])
1807
1808        # --- calculating some portfolio statistics:
1809        byComp = {}  # distribution by companies
1810        bySect = {}  # distribution by sectors
1811        byCurr = {}  # distribution by currencies (include RUB)
1812        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1813        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1814
1815        for item in portfolioResponse["positions"]:
1816            self._figi = item["figi"]
1817            if not self._figi and item["ticker"]:
1818                self._ticker = item["ticker"]
1819                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1820
1821            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1822
1823            if instrument:
1824                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1825                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1826
1827                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1828                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1829
1830                else:
1831                    blocked = 0
1832
1833                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1834                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1835                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1836                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1837                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1838                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1839                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1840                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1841                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1842                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1843                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1844                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1845
1846                statData = {
1847                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1848                    "ticker": instrument["ticker"],  # ticker by FIGI
1849                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1850                    "volume": volume,  # available volume of instrument
1851                    "lots": lots,  # volume in lots of instrument
1852                    "direction": direction,  # direction of an instrument's position: short or long
1853                    "blocked": blocked,  # blocked volume of currency or instrument
1854                    "currentPrice": curPrice,  # current instrument's price in basic asset
1855                    "average": average,  # current average position price
1856                    "cost": cost,  # current cost of all volume of instrument in basic asset
1857                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1858                    "costRUB": costRUB,  # cost of instrument in ruble
1859                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1860                    "profit": profit,  # expected profit at current moment
1861                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1862                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1863                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1864                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1865                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1866                    "step": instrument["step"],  # minimum price increment
1867                }
1868
1869                # adding distribution by unique countries:
1870                if statData["country"] not in byCountry.keys():
1871                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1872
1873                else:
1874                    byCountry[statData["country"]]["cost"] += costRUB
1875                    byCountry[statData["country"]]["percent"] += percentCostRUB
1876
1877                if item["instrumentType"] != "currency":
1878                    # adding distribution by unique companies:
1879                    if statData["name"]:
1880                        if statData["name"] not in byComp.keys():
1881                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1882
1883                        else:
1884                            byComp[statData["name"]]["cost"] += costRUB
1885                            byComp[statData["name"]]["percent"] += percentCostRUB
1886
1887                    # adding distribution by unique sectors:
1888                    if statData["sector"] not in bySect.keys():
1889                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1890
1891                    else:
1892                        bySect[statData["sector"]]["cost"] += costRUB
1893                        bySect[statData["sector"]]["percent"] += percentCostRUB
1894
1895                # adding distribution by unique currencies:
1896                if currency not in byCurr.keys():
1897                    byCurr[currency] = {
1898                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1899                        "cost": costRUB,
1900                        "percent": percentCostRUB
1901                    }
1902
1903                else:
1904                    byCurr[currency]["cost"] += costRUB
1905                    byCurr[currency]["percent"] += percentCostRUB
1906
1907                # saving statistics for every instrument:
1908                if item["instrumentType"] == "currency":
1909                    view["stat"]["Currencies"].append(statData)
1910
1911                    # update dict with free funds for trading (total - blocked) by currencies
1912                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1913                    view["stat"]["funds"][currency] = {
1914                        "total": volume,
1915                        "totalCostRUB": costRUB,  # total volume cost in rubles
1916                        "free": volume - blocked,
1917                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1918                    }
1919
1920                elif item["instrumentType"] == "share":
1921                    view["stat"]["Shares"].append(statData)
1922
1923                elif item["instrumentType"] == "bond":
1924                    view["stat"]["Bonds"].append(statData)
1925
1926                elif item["instrumentType"] == "etf":
1927                    view["stat"]["Etfs"].append(statData)
1928
1929                elif item["instrumentType"] == "Futures":
1930                    view["stat"]["Futures"].append(statData)
1931
1932                else:
1933                    continue
1934
1935        # total changes in Russian Ruble:
1936        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1937        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1938        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1939        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1940        view["stat"]["funds"]["rub"] = {
1941            "total": view["stat"]["availableRUB"],
1942            "totalCostRUB": view["stat"]["availableRUB"],
1943            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1944            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1945        }
1946
1947        # --- pending limit orders sector data:
1948        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1949        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1950
1951        for item in view["raw"]["orders"]:
1952            self._figi = item["figi"]
1953
1954            if item["figi"] not in uniquePendingOrdersFIGIs:
1955                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1956
1957                uniquePendingOrdersFIGIs.append(item["figi"])
1958                uniquePendingOrders[item["figi"]] = instrument
1959
1960            else:
1961                instrument = uniquePendingOrders[item["figi"]]
1962
1963            if instrument:
1964                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1965                orderType = TKS_ORDER_TYPES[item["orderType"]]
1966                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1967                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1968
1969                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1970                if item["direction"] == "ORDER_DIRECTION_BUY":
1971                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1972
1973                else:
1974                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1975
1976                # requested price for order execution:
1977                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1978
1979                # necessary changes in percent to reach target from current price:
1980                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1981
1982                view["stat"]["orders"].append({
1983                    "orderID": item["orderId"],  # orderId number parameter of current order
1984                    "figi": item["figi"],  # FIGI identification
1985                    "ticker": instrument["ticker"],  # ticker name by FIGI
1986                    "lotsRequested": item["lotsRequested"],  # requested lots value
1987                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1988                    "currentPrice": lastPrice,  # current instrument's price for defined action
1989                    "targetPrice": target,  # requested price for order execution in base currency
1990                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1991                    "percentChanges": changes,  # changes in percent to target from current price
1992                    "currency": item["currency"],  # instrument's currency name
1993                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1994                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1995                    "status": orderState,  # order status from TKS_ORDER_STATES
1996                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1997                })
1998
1999        # --- stop orders sector data:
2000        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2001        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2002
2003        for item in view["raw"]["stopOrders"]:
2004            self._figi = item["figi"]
2005
2006            if item["figi"] not in uniqueStopOrdersFIGIs:
2007                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2008
2009                uniqueStopOrdersFIGIs.append(item["figi"])
2010                uniqueStopOrders[item["figi"]] = instrument
2011
2012            else:
2013                instrument = uniqueStopOrders[item["figi"]]
2014
2015            if instrument:
2016                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2017                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2018                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2019
2020                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2021                if "expirationTime" in item.keys():
2022                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2023                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2024
2025                else:
2026                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2027                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2028
2029                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2030                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2031                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2032
2033                else:
2034                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2035
2036                # requested price when stop-order executed:
2037                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2038
2039                # price for limit-order, set up when stop-order executed:
2040                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2041
2042                # necessary changes in percent to reach target from current price:
2043                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2044
2045                view["stat"]["stopOrders"].append({
2046                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2047                    "figi": item["figi"],  # FIGI identification
2048                    "ticker": instrument["ticker"],  # ticker name by FIGI
2049                    "lotsRequested": item["lotsRequested"],  # requested lots value
2050                    "currentPrice": lastPrice,  # current instrument's price for defined action
2051                    "targetPrice": target,  # requested price for stop-order execution in base currency
2052                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2053                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2054                    "percentChanges": changes,  # changes in percent to target from current price
2055                    "currency": item["currency"],  # instrument's currency name
2056                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2057                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2058                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2059                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2060                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2061                })
2062
2063        # --- calculating data for analytics section:
2064        # portfolio distribution by assets:
2065        view["analytics"]["distrByAssets"] = {
2066            "Ruble": {
2067                "uniques": 1,
2068                "cost": view["stat"]["availableRUB"],
2069                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2070            },
2071            "Currencies": {
2072                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2073                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2074                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2075            },
2076            "Shares": {
2077                "uniques": len(view["stat"]["Shares"]),
2078                "cost": view["stat"]["sharesCostRUB"],
2079                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2080            },
2081            "Bonds": {
2082                "uniques": len(view["stat"]["Bonds"]),
2083                "cost": view["stat"]["bondsCostRUB"],
2084                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2085            },
2086            "Etfs": {
2087                "uniques": len(view["stat"]["Etfs"]),
2088                "cost": view["stat"]["etfsCostRUB"],
2089                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2090            },
2091            "Futures": {
2092                "uniques": len(view["stat"]["Futures"]),
2093                "cost": view["stat"]["futuresCostRUB"],
2094                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2095            },
2096        }
2097
2098        # portfolio distribution by companies:
2099        view["analytics"]["distrByCompanies"]["All money cash"] = {
2100            "ticker": "",
2101            "cost": view["stat"]["allCurrenciesCostRUB"],
2102            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2103        }
2104        view["analytics"]["distrByCompanies"].update(byComp)
2105
2106        # portfolio distribution by sectors:
2107        view["analytics"]["distrBySectors"]["All money cash"] = {
2108            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2109            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2110        }
2111        view["analytics"]["distrBySectors"].update(bySect)
2112
2113        # portfolio distribution by currencies:
2114        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2115            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2116
2117            if self.moreDebug:
2118                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2119
2120        view["analytics"]["distrByCurrencies"].update(byCurr)
2121        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2122        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2123
2124        # portfolio distribution by countries:
2125        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2126            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2127
2128            if self.moreDebug:
2129                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2130
2131        view["analytics"]["distrByCountries"].update(byCountry)
2132        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2133        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2134
2135        # --- Prepare text statistics overview in human-readable:
2136        if show or onlyFiles:
2137            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2138
2139            # Whatever the value `details`, header not changes:
2140            info = [
2141                "# Client's portfolio\n\n",
2142                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2143                "* **Account ID:** [{}]\n".format(self.accountId),
2144            ]
2145
2146            if details in ["full", "positions", "digest"]:
2147                info.extend([
2148                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2149                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2150                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2151                        view["stat"]["totalChangesRUB"],
2152                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2153                        view["stat"]["totalChangesPercentRUB"],
2154                    ),
2155                ])
2156
2157            if details in ["full", "positions"]:
2158                info.extend([
2159                    "## Open positions\n\n",
2160                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2161                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2162                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2163                        "{:.2f} ({:.2f}) rub".format(
2164                            view["stat"]["availableRUB"],
2165                            view["stat"]["blockedRUB"],
2166                        )
2167                    )
2168                ])
2169
2170                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2171                    return [
2172                        "|                             |                                 |          |              |              |                     |                              |\n",
2173                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2174                            noTradeStr if noTradeStr else typeStr,
2175                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2176                        ),
2177                    ]
2178
2179                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2180                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2181                        "{} [{}]".format(data["ticker"], data["figi"]),
2182                        "{:.2f} ({:.2f}) {}".format(
2183                            data["volume"],
2184                            data["blocked"],
2185                            data["currency"],
2186                        ) if isCurr else "{:.0f} ({:.0f})".format(
2187                            data["volume"],
2188                            data["blocked"],
2189                        ),
2190                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2191                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2192                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2193                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2194                        "{}{:.2f} {} ({}{:.2f}%)".format(
2195                            "+" if data["profit"] > 0 else "",
2196                            data["profit"], data["baseCurrencyName"],
2197                            "+" if data["percentProfit"] > 0 else "",
2198                            data["percentProfit"],
2199                        ),
2200                    )
2201
2202                # --- Show currencies section:
2203                if view["stat"]["Currencies"]:
2204                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2205                    for item in view["stat"]["Currencies"]:
2206                        info.append(_InfoStr(item, isCurr=True))
2207
2208                else:
2209                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2210
2211                # --- Show shares section:
2212                if view["stat"]["Shares"]:
2213                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2214
2215                    for item in view["stat"]["Shares"]:
2216                        info.append(_InfoStr(item))
2217
2218                else:
2219                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2220
2221                # --- Show bonds section:
2222                if view["stat"]["Bonds"]:
2223                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2224
2225                    for item in view["stat"]["Bonds"]:
2226                        info.append(_InfoStr(item))
2227
2228                else:
2229                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2230
2231                # --- Show etfs section:
2232                if view["stat"]["Etfs"]:
2233                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2234
2235                    for item in view["stat"]["Etfs"]:
2236                        info.append(_InfoStr(item))
2237
2238                else:
2239                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2240
2241                # --- Show futures section:
2242                if view["stat"]["Futures"]:
2243                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2244
2245                    for item in view["stat"]["Futures"]:
2246                        info.append(_InfoStr(item))
2247
2248                else:
2249                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2250
2251            if details in ["full", "orders"]:
2252                # --- Show pending limit orders section:
2253                if view["stat"]["orders"]:
2254                    info.extend([
2255                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2256                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2257                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2258                    ])
2259
2260                    for item in view["stat"]["orders"]:
2261                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2262                            "{} [{}]".format(item["ticker"], item["figi"]),
2263                            item["orderID"],
2264                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2265                            "{} {} ({}{:.2f}%)".format(
2266                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2267                                item["baseCurrencyName"],
2268                                "+" if item["percentChanges"] > 0 else "",
2269                                float(item["percentChanges"]),
2270                            ),
2271                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2272                            item["action"],
2273                            item["type"],
2274                            item["date"],
2275                        ))
2276
2277                else:
2278                    info.append("\n## Total pending limit-orders: [0]\n")
2279
2280                # --- Show stop orders section:
2281                if view["stat"]["stopOrders"]:
2282                    info.extend([
2283                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2284                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2285                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2286                    ])
2287
2288                    for item in view["stat"]["stopOrders"]:
2289                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2290                            "{} [{}]".format(item["ticker"], item["figi"]),
2291                            item["orderID"],
2292                            item["lotsRequested"],
2293                            "{} {} ({}{:.2f}%)".format(
2294                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2295                                item["baseCurrencyName"],
2296                                "+" if item["percentChanges"] > 0 else "",
2297                                float(item["percentChanges"]),
2298                            ),
2299                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2300                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2301                            item["action"],
2302                            item["type"],
2303                            item["expType"],
2304                            item["createDate"],
2305                            item["expDate"],
2306                        ))
2307
2308                else:
2309                    info.append("\n## Total stop-orders: [0]\n")
2310
2311            if details in ["full", "analytics"]:
2312                # -- Show analytics section:
2313                if view["stat"]["portfolioCostRUB"] > 0:
2314                    info.extend([
2315                        "\n# Analytics\n\n"
2316                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2317                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2318                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2319                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2320                            view["stat"]["totalChangesRUB"],
2321                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2322                            view["stat"]["totalChangesPercentRUB"],
2323                        ),
2324                        "\n## Portfolio distribution by assets\n"
2325                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2326                        "|------------------------------------|---------|---------|--------------------|\n",
2327                    ])
2328
2329                    for key in view["analytics"]["distrByAssets"].keys():
2330                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2331                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2332                                key,
2333                                view["analytics"]["distrByAssets"][key]["uniques"],
2334                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2335                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2336                            ))
2337
2338                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2339
2340                    info.extend([
2341                        "\n## Portfolio distribution by companies\n"
2342                        "\n| Company                                      | Percent | Current cost       |\n",
2343                        aSepLine,
2344                    ])
2345
2346                    for company in view["analytics"]["distrByCompanies"].keys():
2347                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2348                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2349                                "{}{}".format(
2350                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2351                                    company,
2352                                ),
2353                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2354                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2355                            ))
2356
2357                    info.extend([
2358                        "\n## Portfolio distribution by sectors\n"
2359                        "\n| Sector                                       | Percent | Current cost       |\n",
2360                        aSepLine,
2361                    ])
2362
2363                    for sector in view["analytics"]["distrBySectors"].keys():
2364                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2365                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2366                                sector,
2367                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2368                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2369                            ))
2370
2371                    info.extend([
2372                        "\n## Portfolio distribution by currencies\n"
2373                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2374                        aSepLine,
2375                    ])
2376
2377                    for curr in view["analytics"]["distrByCurrencies"].keys():
2378                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2379                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2380                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2381                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2382                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2383                            ))
2384
2385                    info.extend([
2386                        "\n## Portfolio distribution by countries\n"
2387                        "\n| Assets by country                            | Percent | Current cost       |\n",
2388                        aSepLine,
2389                    ])
2390
2391                    for country in view["analytics"]["distrByCountries"].keys():
2392                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2393                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2394                                country,
2395                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2396                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2397                            ))
2398
2399            if details in ["full", "calendar"]:
2400                # -- Show bonds payment calendar section:
2401                if view["stat"]["Bonds"]:
2402                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2403                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2404                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2405
2406                else:
2407                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2408
2409            infoText = "".join(info)
2410
2411            if show and not onlyFiles:
2412                uLogger.info(infoText)
2413
2414            if details == "full" and self.overviewFile:
2415                filename = self.overviewFile
2416
2417            elif details == "digest" and self.overviewDigestFile:
2418                filename = self.overviewDigestFile
2419
2420            elif details == "positions" and self.overviewPositionsFile:
2421                filename = self.overviewPositionsFile
2422
2423            elif details == "orders" and self.overviewOrdersFile:
2424                filename = self.overviewOrdersFile
2425
2426            elif details == "analytics" and self.overviewAnalyticsFile:
2427                filename = self.overviewAnalyticsFile
2428
2429            elif details == "calendar" and self.overviewBondsCalendarFile:
2430                filename = self.overviewBondsCalendarFile
2431
2432            else:
2433                filename = ""
2434
2435            if filename and (show or onlyFiles):
2436                with open(filename, "w", encoding="UTF-8") as fH:
2437                    fH.write(infoText)
2438
2439                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2440
2441                if self.useHTMLReports:
2442                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2443                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2444                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2445
2446                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2447
2448        return view
2449
2450    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2451        """
2452        Returns history operations between two given dates for current `accountId`.
2453        If `reportFile` string is not empty then also save human-readable report.
2454        Shows some statistical data of closed positions.
2455
2456        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2457        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2458        :param show: if `True` then also prints all records to the console.
2459        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2460        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2461        :return: original list of dictionaries with history of deals records from API ("operations" key):
2462                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2463                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2464        """
2465        if self.accountId is None or not self.accountId:
2466            uLogger.error("Variable `accountId` must be defined for using this method!")
2467            raise Exception("Account ID required")
2468
2469        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2470
2471        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2472
2473        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2474        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2475        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2476        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2477        customStat = {}  # custom statistics in additional to responseJSON
2478
2479        # --- output report in human-readable format:
2480        if self.reportFile and (show or onlyFiles):
2481            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2482            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2483            nextDay = ""
2484
2485            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2486
2487            if len(ops) > 0:
2488                customStat = {
2489                    "opsCount": 0,  # total operations count
2490                    "buyCount": 0,  # buy operations
2491                    "sellCount": 0,  # sell operations
2492                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2493                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2494                    "payIn": {"rub": 0.},  # Deposit brokerage account
2495                    "payOut": {"rub": 0.},  # Withdrawals
2496                    "divs": {"rub": 0.},  # Dividends income
2497                    "coupons": {"rub": 0.},  # Coupon's income
2498                    "brokerCom": {"rub": 0.},  # Service commissions
2499                    "serviceCom": {"rub": 0.},  # Service commissions
2500                    "marginCom": {"rub": 0.},  # Margin commissions
2501                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2502                }
2503
2504                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2505                for item in ops:
2506                    if item["state"] == "OPERATION_STATE_EXECUTED":
2507                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2508
2509                        # count buy operations:
2510                        if "_BUY" in item["operationType"]:
2511                            customStat["buyCount"] += 1
2512
2513                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2514                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2515
2516                            else:
2517                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2518
2519                        # count sell operations:
2520                        elif "_SELL" in item["operationType"]:
2521                            customStat["sellCount"] += 1
2522
2523                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2524                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2525
2526                            else:
2527                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2528
2529                        # count incoming operations:
2530                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2531                            if item["payment"]["currency"] in customStat["payIn"].keys():
2532                                customStat["payIn"][item["payment"]["currency"]] += payment
2533
2534                            else:
2535                                customStat["payIn"][item["payment"]["currency"]] = payment
2536
2537                        # count withdrawals operations:
2538                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2539                            if item["payment"]["currency"] in customStat["payOut"].keys():
2540                                customStat["payOut"][item["payment"]["currency"]] += payment
2541
2542                            else:
2543                                customStat["payOut"][item["payment"]["currency"]] = payment
2544
2545                        # count dividends income:
2546                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2547                            if item["payment"]["currency"] in customStat["divs"].keys():
2548                                customStat["divs"][item["payment"]["currency"]] += payment
2549
2550                            else:
2551                                customStat["divs"][item["payment"]["currency"]] = payment
2552
2553                        # count coupon's income:
2554                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2555                            if item["payment"]["currency"] in customStat["coupons"].keys():
2556                                customStat["coupons"][item["payment"]["currency"]] += payment
2557
2558                            else:
2559                                customStat["coupons"][item["payment"]["currency"]] = payment
2560
2561                        # count broker commissions:
2562                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2563                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2564                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2565
2566                            else:
2567                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2568
2569                        # count service commissions:
2570                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2571                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2572                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2573
2574                            else:
2575                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2576
2577                        # count margin commissions:
2578                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2579                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2580                                customStat["marginCom"][item["payment"]["currency"]] += payment
2581
2582                            else:
2583                                customStat["marginCom"][item["payment"]["currency"]] = payment
2584
2585                        # count withholding taxes:
2586                        elif "_TAX" in item["operationType"]:
2587                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2588                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2589
2590                            else:
2591                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2592
2593                        else:
2594                            continue
2595
2596                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2597
2598                # --- view "Actions" lines:
2599                info.extend([
2600                    "| Report sections            |                               |                              |                      |                        |\n",
2601                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2602                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2603                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2604                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2605                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2606                    ),
2607                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2608                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2609                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2610                    ),
2611                ])
2612
2613                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2614                for key in opsKeys:
2615                    if key == "rub":
2616                        continue
2617
2618                    info.extend([
2619                        "|                            |                               | {:<28} |                      |                        |\n".format(
2620                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2621                        ),
2622                        "|                            |                               | {:<28} |                      |                        |\n".format(
2623                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2624                        ),
2625                    ])
2626
2627                info.append(splitLine1)
2628
2629                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2630                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2631                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2632                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2633                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2634                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2635                    )
2636
2637                # --- view "Payments" lines:
2638                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2639                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2640
2641                for key in paymentsKeys:
2642                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2643
2644                info.append(splitLine1)
2645
2646                # --- view "Commissions and taxes" lines:
2647                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2648                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2649
2650                for key in comKeys:
2651                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2652
2653                info.extend([
2654                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2655                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2656                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2657                ])
2658
2659            else:
2660                info.append("Broker returned no operations during this period\n")
2661
2662            # --- view "Operations" section:
2663            for item in ops:
2664                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2665                    continue
2666
2667                else:
2668                    self._figi = item["figi"]
2669                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2670                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2671
2672                    # group of deals during one day:
2673                    if nextDay and item["date"].split("T")[0] != nextDay:
2674                        info.append(splitLine2)
2675                        nextDay = ""
2676
2677                    else:
2678                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2679
2680                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2681                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2682                        self._figi if self._figi else "—",
2683                        instrument["ticker"] if instrument else "—",
2684                        instrument["type"] if instrument else "—",
2685                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2686                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2687                        TKS_OPERATION_STATES[item["state"]],
2688                        TKS_OPERATION_TYPES[item["operationType"]],
2689                    ))
2690
2691            infoText = "".join(info)
2692
2693            if show and not onlyFiles:
2694                if self.moreDebug:
2695                    uLogger.debug("Records about history of a client's operations successfully received")
2696
2697                uLogger.info(infoText)
2698
2699            if self.reportFile and (show or onlyFiles):
2700                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2701                    fH.write(infoText)
2702
2703                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2704
2705                if self.useHTMLReports:
2706                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2707                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2708                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2709
2710                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2711
2712        return ops, customStat
2713
2714    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2715        """
2716        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2717
2718        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2719        Warning! Broker server used ISO UTC time by default.
2720
2721        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2722        Also, `historyFile` used to update history with `onlyMissing` parameter.
2723
2724        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2725
2726        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2727        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2728        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2729                         `"hour"`, `"day"`. Default: `"hour"`.
2730        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2731                            False by default. Warning! History appends only from last candle to current time
2732                            with always update last candle!
2733        :param csvSep: separator if csv-file is used, `,` by default.
2734        :param show: if `True` then also prints Pandas DataFrame to the console.
2735        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2736        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2737                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2738        """
2739        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2740        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2741        history = None  # empty pandas object for history
2742
2743        if interval not in TKS_CANDLE_INTERVALS.keys():
2744            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2745            raise Exception("Incorrect value")
2746
2747        if not (self._ticker or self._figi):
2748            uLogger.error("Ticker or FIGI must be defined!")
2749            raise Exception("Ticker or FIGI required")
2750
2751        if self._ticker and not self._figi:
2752            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2753            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2754
2755        if self._figi and not self._ticker:
2756            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2757            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2758
2759        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2760        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2761        if interval.lower() != "day":
2762            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2763
2764        delta = dtEnd - dtStart  # current UTC time minus last time in file
2765        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2766
2767        # calculate history length in candles:
2768        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2769        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2770            length += 1  # to avoid fraction time
2771
2772        # calculate data blocks count:
2773        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2774
2775        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2776        if self.moreDebug:
2777            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2778            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2779            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2780            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2781
2782        tempOld = None  # pandas object for old history, if --only-missing key present
2783        lastTime = None  # datetime object of last old candle in file
2784
2785        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2786            if self.moreDebug:
2787                uLogger.debug("--only-missing key present, add only last missing candles...")
2788                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2789
2790            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2791
2792            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2793            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2794            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2795            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2796
2797            # get last datetime object from last string in file or minus 1 delta if file is empty:
2798            if len(tempOld) > 0:
2799                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2800
2801            else:
2802                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2803
2804            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2805
2806        responseJSONs = []  # raw history blocks of data
2807
2808        blockEnd = dtEnd
2809        for item in range(blocks):
2810            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2811            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2812
2813            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2814                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2815            ))
2816
2817            if blockStart == blockEnd:
2818                uLogger.debug("Skipped this zero-length block...")
2819
2820            else:
2821                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2822                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2823                self.body = str({
2824                    "figi": self._figi,
2825                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2826                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2827                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2828                })
2829                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2830
2831                if "code" in responseJSON.keys():
2832                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2833
2834                else:
2835                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2836                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2837
2838                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2839
2840            blockEnd = blockStart
2841
2842        printCount = len(responseJSONs)  # candles to show in console
2843        if responseJSONs:
2844            tempHistory = pd.DataFrame(
2845                data={
2846                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2847                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2848                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2849                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2850                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2851                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2852                    "volume": [int(item["volume"]) for item in responseJSONs],
2853                },
2854                index=range(len(responseJSONs)),
2855                columns=["date", "time", "open", "high", "low", "close", "volume"],
2856            )
2857            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2858            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2859
2860            # append only newest candles to old history if --only-missing key present:
2861            if onlyMissing and tempOld is not None and lastTime is not None:
2862                index = 0  # find start index in tempHistory data:
2863
2864                for i, item in tempHistory.iterrows():
2865                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2866
2867                    if curTime == lastTime:
2868                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2869                        index = i
2870                        printCount = i + 1
2871                        break
2872
2873                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2874
2875            else:
2876                history = tempHistory  # if no `--only-missing` key then load full data from server
2877
2878            if self.moreDebug:
2879                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2880
2881        if history is not None and not history.empty:
2882            if show and not onlyFiles:
2883                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2884                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2885                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2886                ))
2887
2888        else:
2889            uLogger.warning("Received an empty candles history!")
2890
2891        if self.historyFile is not None:
2892            if history is not None and not history.empty:
2893                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2894                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2895
2896            else:
2897                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2898
2899        else:
2900            if self.moreDebug:
2901                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2902
2903        return history
2904
2905    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2906        """
2907        Load candles history from csv-file and return Pandas DataFrame object.
2908
2909        See also: `History()` and `ShowHistoryChart()` methods.
2910
2911        :param filePath: path to csv-file to open.
2912        """
2913        loadedHistory = None  # init candles data object
2914
2915        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2916
2917        if os.path.exists(filePath):
2918            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2919
2920            tfStr = self.priceModel.FormattedDelta(
2921                self.priceModel.timeframe,
2922                "{days} days {hours}h {minutes}m {seconds}s",
2923            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2924                self.priceModel.timeframe,
2925                "{hours}h {minutes}m {seconds}s",
2926            )
2927
2928            if loadedHistory is not None and not loadedHistory.empty:
2929                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2930                    len(loadedHistory),
2931                    tfStr,
2932                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2933                )
2934
2935            else:
2936                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2937
2938        else:
2939            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2940
2941        return loadedHistory
2942
2943    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2944        """
2945        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2946
2947        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2948        Default: `index.html` (both for interact and non-interact candlesticks chart).
2949
2950        See also: `History()` and `LoadHistory()` methods.
2951
2952        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2953        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2954                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2955                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2956                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2957        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2958                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2959        """
2960        if isinstance(candles, str):
2961            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2962            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2963
2964        elif isinstance(candles, pd.DataFrame):
2965            self.priceModel.prices = candles  # set candles chain from variable
2966            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2967
2968            if "datetime" not in candles.columns:
2969                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2970
2971        else:
2972            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2973            raise Exception("Incorrect value")
2974
2975        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2976
2977        if interact:
2978            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2979
2980            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2981
2982        else:
2983            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2984
2985            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2986
2987        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2988
2989    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2990        """
2991        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2992        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2993
2994        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2995
2996        :param operation: string "Buy" or "Sell".
2997        :param lots: volume, integer count of lots >= 1.
2998        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2999        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3000        :param expDate: string "Undefined" by default or local date in future,
3001                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3002        :return: JSON with response from broker server.
3003        """
3004        if self.accountId is None or not self.accountId:
3005            uLogger.error("Variable `accountId` must be defined for using this method!")
3006            raise Exception("Account ID required")
3007
3008        if operation is None or not operation or operation not in ("Buy", "Sell"):
3009            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3010            raise Exception("Incorrect value")
3011
3012        if lots is None or lots < 1:
3013            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3014            lots = 1
3015
3016        if tp is None or tp < 0:
3017            tp = 0
3018
3019        if sl is None or sl < 0:
3020            sl = 0
3021
3022        if expDate is None or not expDate:
3023            expDate = "Undefined"
3024
3025        if not (self._ticker or self._figi):
3026            uLogger.error("Ticker or FIGI must be defined!")
3027            raise Exception("Ticker or FIGI required")
3028
3029        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3030        self._ticker = instrument["ticker"]
3031        self._figi = instrument["figi"]
3032
3033        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3034
3035        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3036        self.body = str({
3037            "figi": self._figi,
3038            "quantity": str(lots),
3039            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3040            "accountId": str(self.accountId),
3041            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3042        })
3043        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3044
3045        if "orderId" in response.keys():
3046            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3047                operation, response["orderId"],
3048                self._ticker, self._figi, lots,
3049                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3050                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3051                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3052            ))
3053
3054            if tp > 0:
3055                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3056
3057            if sl > 0:
3058                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3059
3060        else:
3061            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3062
3063        return response
3064
3065    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3066        """
3067        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3068        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3069
3070        See also: `Order()` and `Trade()` docstrings.
3071
3072        :param lots: volume, integer count of lots >= 1.
3073        :param tp: float > 0, take profit price of stop-order.
3074        :param sl: float > 0, stop loss price of stop-order.
3075        :param expDate: it's a local date in future.
3076                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3077        :return: JSON with response from broker server.
3078        """
3079        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3080
3081    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3082        """
3083        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3084        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3085
3086        See also: `Order()` and `Trade()` docstrings.
3087
3088        :param lots: volume, integer count of lots >= 1.
3089        :param tp: float > 0, take profit price of stop-order.
3090        :param sl: float > 0, stop loss price of stop-order.
3091        :param expDate: it's a local date in the future.
3092                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3093        :return: JSON with response from broker server.
3094        """
3095        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3096
3097    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3098        """
3099        Close position of given instruments.
3100
3101        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3102        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3103                         This avoids unnecessary downloading data from the server.
3104        """
3105        if instruments is None or not instruments:
3106            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3107            raise Exception("Ticker or FIGI required")
3108
3109        if isinstance(instruments, str):
3110            instruments = [instruments]
3111
3112        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3113        if uniqueInstruments:
3114            if portfolio is None or not portfolio:
3115                portfolio = self.Overview(show=False)
3116
3117            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3118            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3119
3120            for self._figi in uniqueInstruments:
3121                if self._figi not in allOpened:
3122                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3123                    continue
3124
3125                # search open trade info about instrument by ticker:
3126                instrument = {}
3127                for iType in TKS_INSTRUMENTS:
3128                    if instrument:
3129                        break
3130
3131                    for item in portfolio["stat"][iType]:
3132                        if item["figi"] == self._figi:
3133                            instrument = item
3134                            break
3135
3136                if instrument:
3137                    self._ticker = instrument["ticker"]
3138                    self._figi = instrument["figi"]
3139
3140                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3141                        self._ticker,
3142                        self._figi,
3143                        int(instrument["volume"]),
3144                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3145                    ))
3146
3147                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3148
3149                    if tradeLots > 0:
3150                        if instrument["blocked"] > 0:
3151                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3152                                instrument["blocked"],
3153                                self._ticker,
3154                                tradeLots,
3155                            ))
3156
3157                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3158                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3159
3160                    else:
3161                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3162
3163    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3164        """
3165        Close all positions of given instruments with defined type.
3166
3167        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3168        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3169                         This avoids unnecessary downloading data from the server.
3170        """
3171        if iType not in TKS_INSTRUMENTS:
3172            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3173
3174        else:
3175            if portfolio is None or not portfolio:
3176                portfolio = self.Overview(show=False)
3177
3178            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3179            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3180
3181            if tickers and portfolio:
3182                self.CloseTrades(tickers, portfolio)
3183
3184            else:
3185                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3186
3187    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3188        """
3189        Universal method to create market or limit orders with all available parameters for current `accountId`.
3190        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3191
3192        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3193        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3194
3195        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3196        then broker immediately open market order as you can do simple --buy or --sell operations!
3197
3198        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3199        When current price will go up or down to target price value then broker opens a limit order.
3200        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3201
3202        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3203
3204        :param operation: string "Buy" or "Sell".
3205        :param orderType: string "Limit" or "Stop".
3206        :param lots: volume, integer count of lots >= 1.
3207        :param targetPrice: target price > 0. This is open trade price for limit order.
3208        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3209                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3210        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3211                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3212                         Stop loss order always executed by market price.
3213        :param expDate: string "Undefined" by default or local date in future.
3214                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3215                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3216                        A limit order has no expiration date, it lasts until the end of the trading day.
3217        :return: JSON with response from broker server.
3218        """
3219        if self.accountId is None or not self.accountId:
3220            uLogger.error("Variable `accountId` must be defined for using this method!")
3221            raise Exception("Account ID required")
3222
3223        if operation is None or not operation or operation not in ("Buy", "Sell"):
3224            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3225            raise Exception("Incorrect value")
3226
3227        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3228            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3229            raise Exception("Incorrect value")
3230
3231        if lots is None or lots < 1:
3232            uLogger.error("You must define trade volume > 0: integer count of lots!")
3233            raise Exception("Incorrect value")
3234
3235        if targetPrice is None or targetPrice <= 0:
3236            uLogger.error("Target price for limit-order must be greater than 0!")
3237            raise Exception("Incorrect value")
3238
3239        if limitPrice is None or limitPrice <= 0:
3240            limitPrice = targetPrice
3241
3242        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3243            stopType = "Limit"
3244
3245        if expDate is None or not expDate:
3246            expDate = "Undefined"
3247
3248        if not (self._ticker or self._figi):
3249            uLogger.error("Tocker or FIGI must be defined!")
3250            raise Exception("Ticker or FIGI required")
3251
3252        response = {}
3253        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3254        self._ticker = instrument["ticker"]
3255        self._figi = instrument["figi"]
3256
3257        if orderType == "Limit":
3258            uLogger.debug(
3259                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3260                    self._ticker, self._figi,
3261                    operation, lots, targetPrice, instrument["currency"],
3262                ))
3263
3264            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3265            self.body = str({
3266                "figi": self._figi,
3267                "quantity": str(lots),
3268                "price": FloatToNano(targetPrice),
3269                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3270                "accountId": str(self.accountId),
3271                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3272            })
3273            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3274
3275            if "orderId" in response.keys():
3276                uLogger.info(
3277                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3278                        response["orderId"], self._ticker, self._figi, operation, lots,
3279                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3280                    ))
3281
3282                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3283                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3284                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3285                            targetPrice, instrument["currency"],
3286                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3287                        ))
3288
3289                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3290                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3291                            targetPrice, instrument["currency"],
3292                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3293                        ))
3294
3295            else:
3296                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3297
3298        if orderType == "Stop":
3299            uLogger.debug(
3300                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3301                    self._ticker, self._figi,
3302                    operation, lots,
3303                    targetPrice, instrument["currency"],
3304                    limitPrice, instrument["currency"],
3305                    stopType, expDate,
3306                ))
3307
3308            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3309            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3310            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3311
3312            body = {
3313                "figi": self._figi,
3314                "quantity": str(lots),
3315                "price": FloatToNano(limitPrice),
3316                "stopPrice": FloatToNano(targetPrice),
3317                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3318                "accountId": str(self.accountId),
3319                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3320                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3321            }
3322
3323            if expDateUTC:
3324                body["expireDate"] = expDateUTC
3325
3326            self.body = str(body)
3327            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3328
3329            if "stopOrderId" in response.keys():
3330                uLogger.info(
3331                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3332                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3333                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3334                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3335                        TKS_STOP_ORDER_TYPES[stopOrderType],
3336                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3337                    ))
3338
3339                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3340                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3341                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3342                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3343                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3344                        ))
3345
3346                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3347                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3348                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3349                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3350                        ))
3351
3352            else:
3353                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3354
3355        return response
3356
3357    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3358        """
3359        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3360        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3361        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3362        See also: `Order()` docstring.
3363
3364        :param lots: volume, integer count of lots >= 1.
3365        :param targetPrice: target price > 0. This is open trade price for limit order.
3366        :return: JSON with response from broker server.
3367        """
3368        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3369
3370    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3371        """
3372        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3373        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3374        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3375        target price value then broker opens a limit order. See also: `Order()` docstring.
3376
3377        :param lots: volume, integer count of lots >= 1.
3378        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3379        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3380                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3381        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3382                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3383        :param expDate: string "Undefined" by default or local date in future.
3384                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3385                        This date is converting to UTC format for server.
3386        :return: JSON with response from broker server.
3387        """
3388        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3389
3390    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3391        """
3392        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3393        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3394        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3395        See also: `Order()` docstring.
3396
3397        :param lots: volume, integer count of lots >= 1.
3398        :param targetPrice: target price > 0. This is open trade price for limit order.
3399        :return: JSON with response from broker server.
3400        """
3401        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3402
3403    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3404        """
3405        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3406        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3407        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3408        target price value then broker opens a limit order. See also: `Order()` docstring.
3409
3410        :param lots: volume, integer count of lots >= 1.
3411        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3412        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3413                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3414        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3415                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3416        :param expDate: string "Undefined" by default or local date in future.
3417                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3418                        This date is converting to UTC format for server.
3419        :return: JSON with response from broker server.
3420        """
3421        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3422
3423    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3424        """
3425        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3426
3427        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3428        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3429                             This avoids unnecessary downloading data from the server.
3430        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3431        """
3432        if self.accountId is None or not self.accountId:
3433            uLogger.error("Variable `accountId` must be defined for using this method!")
3434            raise Exception("Account ID required")
3435
3436        if orderIDs:
3437            if allOrdersIDs is None:
3438                rawOrders = self.RequestPendingOrders()
3439                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3440
3441            if allStopOrdersIDs is None:
3442                rawStopOrders = self.RequestStopOrders()
3443                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3444
3445            for orderID in orderIDs:
3446                idInPendingOrders = orderID in allOrdersIDs
3447                idInStopOrders = orderID in allStopOrdersIDs
3448
3449                if not (idInPendingOrders or idInStopOrders):
3450                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3451                    continue
3452
3453                else:
3454                    if idInPendingOrders:
3455                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3456
3457                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3458                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3459                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3460                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3461
3462                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3463                            if self.moreDebug:
3464                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3465
3466                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3467
3468                        else:
3469                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3470
3471                    elif idInStopOrders:
3472                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3473
3474                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3475                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3476                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3477                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3478
3479                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3480                            if self.moreDebug:
3481                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3482
3483                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3484
3485                        else:
3486                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3487
3488                    else:
3489                        continue
3490
3491    def CloseAllOrders(self) -> None:
3492        """
3493        Gets a list of open pending and stop orders and cancel it all.
3494        """
3495        rawOrders = self.RequestPendingOrders()
3496        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3497        lenOrders = len(allOrdersIDs)
3498
3499        rawStopOrders = self.RequestStopOrders()
3500        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3501        lenSOrders = len(allStopOrdersIDs)
3502
3503        if lenOrders > 0 or lenSOrders > 0:
3504            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3505
3506            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3507
3508        else:
3509            uLogger.info("Orders not found, nothing to cancel.")
3510
3511    def CloseAll(self, *args) -> None:
3512        """
3513        Close all available (not blocked) opened trades and orders.
3514
3515        Also, you can select one or more keywords case-insensitive:
3516        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3517
3518        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3519        """
3520        overview = self.Overview(show=False)  # get all open trades info
3521
3522        if len(args) == 0:
3523            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3524            self.CloseAllOrders()  # close all pending and stop orders
3525
3526            for iType in TKS_INSTRUMENTS:
3527                if iType != "Currencies":
3528                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3529
3530        else:
3531            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3532            lowerArgs = [x.lower() for x in args]
3533
3534            if "orders" in lowerArgs:
3535                self.CloseAllOrders()  # close all pending and stop orders
3536
3537            for iType in TKS_INSTRUMENTS:
3538                if iType.lower() in lowerArgs and iType != "Currencies":
3539                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3540
3541    def CloseAllByTicker(self, instrument: str) -> None:
3542        """
3543        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3544
3545        This method searches opened trade and orders of instrument throw all portfolio and then use
3546        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3547
3548        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3549
3550        :param instrument: string with ticker.
3551        """
3552        if instrument is None or not instrument:
3553            uLogger.error("Ticker name must be defined for using this method!")
3554            raise Exception("Ticker required")
3555
3556        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3557
3558        self._ticker = instrument  # try to set instrument as ticker
3559        self._figi = ""
3560
3561        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3562        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3563
3564        if limitAll and self.IsInLimitOrders(portfolio=overview):
3565            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3566            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3567
3568        if stopAll and self.IsInStopOrders(portfolio=overview):
3569            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3570            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3571
3572        if self.IsInPortfolio(portfolio=overview):
3573            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3574            self.CloseTrades(instruments=[instrument], portfolio=overview)
3575
3576    def CloseAllByFIGI(self, instrument: str) -> None:
3577        """
3578        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3579
3580        This method searches opened trade and orders of instrument throw all portfolio and then use
3581        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3582
3583        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3584
3585        :param instrument: string with FIGI id.
3586        """
3587        if instrument is None or not instrument:
3588            uLogger.error("FIGI id must be defined for using this method!")
3589            raise Exception("FIGI required")
3590
3591        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3592
3593        self._ticker = ""
3594        self._figi = instrument  # try to set instrument as FIGI id
3595
3596        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3597        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3598
3599        if limitAll and self.IsInLimitOrders(portfolio=overview):
3600            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3601            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3602
3603        if stopAll and self.IsInStopOrders(portfolio=overview):
3604            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3605            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3606
3607        if self.IsInPortfolio(portfolio=overview):
3608            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3609            self.CloseTrades(instruments=[instrument], portfolio=overview)
3610
3611    @staticmethod
3612    def ParseOrderParameters(operation, **inputParameters):
3613        """
3614        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3615
3616        :param operation: string "Buy" or "Sell".
3617        :param inputParameters: this is dict of strings that looks like this
3618               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3619               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3620               "prices" key: one or more prices to open limit-orders
3621               Counts of values in lots and prices lists must be equals!
3622        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3623        """
3624        # TODO: update order grid work with api v2
3625        pass
3626        # uLogger.debug("Input parameters: {}".format(inputParameters))
3627        #
3628        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3629        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3630        #     raise Exception("Incorrect value")
3631        #
3632        # if "l" in inputParameters.keys():
3633        #     inputParameters["lots"] = inputParameters.pop("l")
3634        #
3635        # if "p" in inputParameters.keys():
3636        #     inputParameters["prices"] = inputParameters.pop("p")
3637        #
3638        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3639        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3640        #     raise Exception("Incorrect value")
3641        #
3642        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3643        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3644        #
3645        # if len(lots) != len(prices):
3646        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3647        #     raise Exception("Incorrect value")
3648        #
3649        # uLogger.debug("Extracted parameters for orders:")
3650        # uLogger.debug("lots = {}".format(lots))
3651        # uLogger.debug("prices = {}".format(prices))
3652        #
3653        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3654        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3655        # uLogger.debug("Order parameters: {}".format(result))
3656        #
3657        # return result
3658
3659    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3660        """
3661        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3662
3663        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3664        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3665        """
3666        result = False
3667        msg = "Instrument not defined!"
3668
3669        if portfolio is None or not portfolio:
3670            portfolio = self.Overview(show=False)
3671
3672        if self._ticker:
3673            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3674            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3675
3676            for iType in TKS_INSTRUMENTS:
3677                for instrument in portfolio["stat"][iType]:
3678                    if instrument["ticker"] == self._ticker:
3679                        result = True
3680                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3681                        break
3682
3683        elif self._figi:
3684            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3685            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3686
3687            for iType in TKS_INSTRUMENTS:
3688                for instrument in portfolio["stat"][iType]:
3689                    if instrument["figi"] == self._figi:
3690                        result = True
3691                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3692                        break
3693
3694        else:
3695            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3696
3697        uLogger.debug(msg)
3698
3699        return result
3700
3701    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3702        """
3703        Returns instrument from the user's portfolio if it presents there.
3704        Instrument must be defined by `ticker` (highly priority) or `figi`.
3705
3706        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3707        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3708        """
3709        result = None
3710        msg = "Instrument not defined!"
3711
3712        if portfolio is None or not portfolio:
3713            portfolio = self.Overview(show=False)
3714
3715        if self._ticker:
3716            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3717            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3718
3719            for iType in TKS_INSTRUMENTS:
3720                for instrument in portfolio["stat"][iType]:
3721                    if instrument["ticker"] == self._ticker:
3722                        result = instrument
3723                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3724                        break
3725
3726        elif self._figi:
3727            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3728            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3729
3730            for iType in TKS_INSTRUMENTS:
3731                for instrument in portfolio["stat"][iType]:
3732                    if instrument["figi"] == self._figi:
3733                        result = instrument
3734                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3735                        break
3736
3737        else:
3738            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3739
3740        uLogger.debug(msg)
3741
3742        return result
3743
3744    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3745        """
3746        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3747
3748        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3749
3750        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3751        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3752        """
3753        result = False
3754        msg = "Instrument not defined!"
3755
3756        if portfolio is None or not portfolio:
3757            portfolio = self.Overview(show=False)
3758
3759        if self._ticker:
3760            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3761            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3762
3763            for instrument in portfolio["stat"]["orders"]:
3764                if instrument["ticker"] == self._ticker:
3765                    result = True
3766                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3767                    break
3768
3769        elif self._figi:
3770            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3771            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3772
3773            for instrument in portfolio["stat"]["orders"]:
3774                if instrument["figi"] == self._figi:
3775                    result = True
3776                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3777                    break
3778
3779        else:
3780            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3781
3782        uLogger.debug(msg)
3783
3784        return result
3785
3786    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3787        """
3788        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3789        Instrument must be defined by `ticker` (highly priority) or `figi`.
3790
3791        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3792
3793        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3794        :return: list with `orderID`s of limit orders.
3795        """
3796        result = []
3797        msg = "Instrument not defined!"
3798
3799        if portfolio is None or not portfolio:
3800            portfolio = self.Overview(show=False)
3801
3802        if self._ticker:
3803            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3804            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3805
3806            for instrument in portfolio["stat"]["orders"]:
3807                if instrument["ticker"] == self._ticker:
3808                    result.append(instrument["orderID"])
3809
3810            if result:
3811                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3812
3813        elif self._figi:
3814            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3815            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3816
3817            for instrument in portfolio["stat"]["orders"]:
3818                if instrument["figi"] == self._figi:
3819                    result.append(instrument["orderID"])
3820
3821            if result:
3822                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3823
3824        else:
3825            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3826
3827        uLogger.debug(msg)
3828
3829        return result
3830
3831    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3832        """
3833        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3834
3835        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3836
3837        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3838        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3839        """
3840        result = False
3841        msg = "Instrument not defined!"
3842
3843        if portfolio is None or not portfolio:
3844            portfolio = self.Overview(show=False)
3845
3846        if self._ticker:
3847            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3848            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3849
3850            for instrument in portfolio["stat"]["stopOrders"]:
3851                if instrument["ticker"] == self._ticker:
3852                    result = True
3853                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3854                    break
3855
3856        elif self._figi:
3857            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3858            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3859
3860            for instrument in portfolio["stat"]["stopOrders"]:
3861                if instrument["figi"] == self._figi:
3862                    result = True
3863                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3864                    break
3865
3866        else:
3867            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3868
3869        uLogger.debug(msg)
3870
3871        return result
3872
3873    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3874        """
3875        Returns list with all `orderID`s of opened stop orders for the instrument.
3876        Instrument must be defined by `ticker` (highly priority) or `figi`.
3877
3878        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3879
3880        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3881        :return: list with `orderID`s of stop orders.
3882        """
3883        result = []
3884        msg = "Instrument not defined!"
3885
3886        if portfolio is None or not portfolio:
3887            portfolio = self.Overview(show=False)
3888
3889        if self._ticker:
3890            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3891            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3892
3893            for instrument in portfolio["stat"]["stopOrders"]:
3894                if instrument["ticker"] == self._ticker:
3895                    result.append(instrument["orderID"])
3896
3897            if result:
3898                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3899
3900        elif self._figi:
3901            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3902            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3903
3904            for instrument in portfolio["stat"]["stopOrders"]:
3905                if instrument["figi"] == self._figi:
3906                    result.append(instrument["orderID"])
3907
3908            if result:
3909                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3910
3911        else:
3912            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3913
3914        uLogger.debug(msg)
3915
3916        return result
3917
3918    def RequestLimits(self) -> dict:
3919        """
3920        Method for obtaining the available funds for withdrawal for current `accountId`.
3921
3922        See also:
3923        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3924        - `OverviewLimits()` method
3925
3926        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3927                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3928                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3929                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3930        """
3931        if self.accountId is None or not self.accountId:
3932            uLogger.error("Variable `accountId` must be defined for using this method!")
3933            raise Exception("Account ID required")
3934
3935        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3936
3937        self.body = str({"accountId": self.accountId})
3938        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3939        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3940
3941        if self.moreDebug:
3942            uLogger.debug("Records about available funds for withdrawal successfully received")
3943
3944        return rawLimits
3945
3946    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3947        """
3948        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3949
3950        See also: `RequestLimits()`.
3951
3952        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3953        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3954        :return: dict with raw parsed data from server and some calculated statistics about it.
3955        """
3956        if self.accountId is None or not self.accountId:
3957            uLogger.error("Variable `accountId` must be defined for using this method!")
3958            raise Exception("Account ID required")
3959
3960        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3961
3962        view = {
3963            "rawLimits": rawLimits,
3964            "limits": {  # parsed data for every currency:
3965                "money": {  # this is an array of portfolio currency positions
3966                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3967                },
3968                "blocked": {  # this is an array of blocked currency
3969                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3970                },
3971                "blockedGuarantee": {  # this is locked money under collateral for futures
3972                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3973                },
3974            },
3975        }
3976
3977        # --- Prepare text table with limits in human-readable format:
3978        if show or onlyFiles:
3979            info = [
3980                "# Withdrawal limits\n\n",
3981                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3982                "* **Account ID:** [{}]\n".format(self.accountId),
3983            ]
3984
3985            if view["limits"]["money"]:
3986                info.extend([
3987                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3988                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3989                ])
3990
3991            else:
3992                info.append("\nNo withdrawal limits\n")
3993
3994            for curr in view["limits"]["money"].keys():
3995                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3996                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3997                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3998
3999                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4000                    "[{}]".format(curr),
4001                    "{:.2f}".format(view["limits"]["money"][curr]),
4002                    "{:.2f}".format(availableMoney),
4003                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4004                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4005                )
4006
4007                if curr == "rub":
4008                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4009
4010                else:
4011                    info.append(infoStr)
4012
4013            infoText = "".join(info)
4014
4015            if show and not onlyFiles:
4016                uLogger.info(infoText)
4017
4018            if self.withdrawalLimitsFile and (show or onlyFiles):
4019                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4020                    fH.write(infoText)
4021
4022                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4023
4024                if self.useHTMLReports:
4025                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4026                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4027                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4028
4029                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4030
4031        return view
4032
4033    def RequestAccounts(self) -> dict:
4034        """
4035        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4036
4037        See also:
4038        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4039        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4040        - `OverviewUserInfo()` method
4041
4042        :return: dict with raw data from server that contains accounts info. Example of dict:
4043                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4044                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4045                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4046                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4047        """
4048        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4049
4050        self.body = str({})
4051        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4052        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4053
4054        if self.moreDebug:
4055            uLogger.debug("Records about available accounts successfully received")
4056
4057        return rawAccounts
4058
4059    def RequestUserInfo(self) -> dict:
4060        """
4061        Method for requesting common user's information.
4062
4063        See also:
4064        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4065        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4066        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4067        - `OverviewUserInfo()` method
4068
4069        :return: dict with raw data from server that contains user's information. Example of dict:
4070                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4071                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4072        """
4073        uLogger.debug("Requesting common user's information. Wait, please...")
4074
4075        self.body = str({})
4076        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4077        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4078
4079        if self.moreDebug:
4080            uLogger.debug("Records about current user successfully received")
4081
4082        return rawUserInfo
4083
4084    def RequestMarginStatus(self, accountId: str = None) -> dict:
4085        """
4086        Method for requesting margin calculation for defined account ID.
4087
4088        See also:
4089        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4090        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4091        - `OverviewUserInfo()` method
4092
4093        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4094        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4095                 Example of responses:
4096                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4097                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4098                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4099                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4100                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4101                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4102        """
4103        if accountId is None or not accountId:
4104            if self.accountId is None or not self.accountId:
4105                uLogger.error("Variable `accountId` must be defined for using this method!")
4106                raise Exception("Account ID required")
4107
4108            else:
4109                accountId = self.accountId  # use `self.accountId` (main ID) by default
4110
4111        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4112
4113        self.body = str({"accountId": accountId})
4114        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4115        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4116
4117        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4118            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4119            rawMargin = {}
4120
4121        else:
4122            if self.moreDebug:
4123                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4124
4125        return rawMargin
4126
4127    def RequestTariffLimits(self) -> dict:
4128        """
4129        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4130
4131        See also:
4132        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4133        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4134        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4135        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4136        - `OverviewUserInfo()` method
4137
4138        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4139                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4140                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4141        """
4142        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4143
4144        self.body = str({})
4145        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4146        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4147
4148        if self.moreDebug:
4149            uLogger.debug("Records with limits of current tariff successfully received")
4150
4151        return rawTariffLimits
4152
4153    def RequestBondCoupons(self, iJSON: dict) -> dict:
4154        """
4155        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4156        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4157        All dates are in UTC timezone.
4158
4159        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4160        Documentation:
4161        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4162        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4163
4164        See also: `ExtendBondsData()`.
4165
4166        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4167                      If raw iJSON is not data of bond then server returns an error [400] with message:
4168                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4169        :return: dictionary with bond payment calendar. Response example
4170                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4171                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4172                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4173                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4174        """
4175        if iJSON["figi"] is None or not iJSON["figi"]:
4176            uLogger.error("FIGI must be defined for using this method!")
4177            raise Exception("FIGI required")
4178
4179        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4180        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4181
4182        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4183            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4184            self._figi,
4185            startDate,
4186            endDate,
4187        ))
4188
4189        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4190        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4191        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4192
4193        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4194            uLogger.warning("Instrument type is not bond!")
4195
4196        else:
4197            if self.moreDebug:
4198                uLogger.debug("Records about bond payment calendar successfully received")
4199
4200        return calendar
4201
4202    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4203        """
4204        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4205        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4206        coupon yields, current yields and some statistics etc.
4207
4208        WARNING! This is too long operation if a lot of bonds requested from broker server.
4209
4210        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4211
4212        :param instruments: list of strings with tickers or FIGIs.
4213        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4214                     for further used by data scientists or stock analytics.
4215        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4216                 In XLSX-file and Pandas DataFrame fields mean:
4217                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4218                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4219        """
4220        if instruments is None or not instruments:
4221            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4222            raise Exception("Ticker or FIGI required")
4223
4224        if isinstance(instruments, str):
4225            instruments = [instruments]
4226
4227        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4228
4229        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4230
4231        iCount = len(uniqueInstruments)
4232        tooLong = iCount >= 20
4233        if tooLong:
4234            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4235
4236        bonds = None
4237        for i, self._figi in enumerate(uniqueInstruments):
4238            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4239
4240            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4241                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4242                rawBond = self.SearchByFIGI(requestPrice=True)
4243
4244                # Widen raw data with UTC current time (iData["actualDateTime"]):
4245                actualDate = datetime.now(tzutc())
4246                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4247
4248                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4249                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4250
4251                # Replace some values with human-readable:
4252                iData["nominalCurrency"] = iData["nominal"]["currency"]
4253                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4254                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4255                iData["aciCurrency"] = iData["aciValue"]["currency"]
4256                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4257                iData["issueSize"] = int(iData["issueSize"])
4258                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4259                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4260                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4261                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4262                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4263                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4264                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4265                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4266                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4267                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4268
4269                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4270                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4271                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4272                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4273                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4274                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4275                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4276                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4277                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4278                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4279                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4280
4281                # Widen raw data with calendar data from `rawCalendar` values:
4282                calendarData = []
4283                if "events" in iData["rawCalendar"].keys():
4284                    for item in iData["rawCalendar"]["events"]:
4285                        calendarData.append({
4286                            "couponDate": item["couponDate"],
4287                            "couponNumber": int(item["couponNumber"]),
4288                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4289                            "payCurrency": item["payOneBond"]["currency"],
4290                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4291                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4292                            "couponStartDate": item["couponStartDate"],
4293                            "couponEndDate": item["couponEndDate"],
4294                            "couponPeriod": item["couponPeriod"],
4295                        })
4296
4297                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4298                    if "maturityDate" not in iData.keys():
4299                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4300
4301                # Widen raw data with Coupon Rate.
4302                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4303                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4304                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4305                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4306
4307                # Widen raw data with Yield to Maturity (YTM) on current date.
4308                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4309                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4310                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4311                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4312                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4313                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4314
4315                iData["calendar"] = calendarData  # adds calendar at the end
4316
4317                # Remove not used data:
4318                iData.pop("uid")
4319                iData.pop("positionUid")
4320                iData.pop("currentPrice")
4321                iData.pop("rawCalendar")
4322
4323                colNames = list(iData.keys())
4324                if bonds is None:
4325                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4326
4327                else:
4328                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4329
4330            else:
4331                uLogger.warning("Instrument is not a bond!")
4332
4333            processed = round(100 * (i + 1) / iCount, 1)
4334            if tooLong and processed % 5 == 0:
4335                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4336
4337            else:
4338                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4339
4340        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4341
4342        # Saving bonds from Pandas DataFrame to XLSX sheet:
4343        if xlsx and self.bondsXLSXFile:
4344            with pd.ExcelWriter(
4345                    path=self.bondsXLSXFile,
4346                    date_format=TKS_DATE_FORMAT,
4347                    datetime_format=TKS_DATE_TIME_FORMAT,
4348                    mode="w",
4349            ) as writer:
4350                bonds.to_excel(
4351                    writer,
4352                    sheet_name="Extended bonds data",
4353                    index=True,
4354                    encoding="UTF-8",
4355                    freeze_panes=(1, 1),
4356                )  # saving as XLSX-file with freeze first row and column as headers
4357
4358            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4359
4360        return bonds
4361
4362    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4363        """
4364        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4365
4366        WARNING! This is too long operation if a lot of bonds requested from broker server.
4367
4368        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4369
4370        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4371                        extended information about bonds: main info, current prices, bond payment calendar,
4372                        coupon yields, current yields and some statistics etc.
4373                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4374        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4375                     for further used by data scientists or stock analytics.
4376        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4377        """
4378        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4379            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4380
4381        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4382
4383        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4384        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4385        calendar = None
4386        for bond in extBonds.iterrows():
4387            for item in bond[1]["calendar"]:
4388                cData = {
4389                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4390                    "couponDate": item["couponDate"],
4391                    "figi": bond[1]["figi"],
4392                    "ticker": bond[1]["ticker"],
4393                    "name": bond[1]["name"],
4394                    "couponNumber": item["couponNumber"],
4395                    "payOneBond": item["payOneBond"],
4396                    "payCurrency": item["payCurrency"],
4397                    "couponType": item["couponType"],
4398                    "couponPeriod": item["couponPeriod"],
4399                    "fixDate": item["fixDate"],
4400                    "couponStartDate": item["couponStartDate"],
4401                    "couponEndDate": item["couponEndDate"],
4402                }
4403
4404                if calendar is None:
4405                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4406
4407                else:
4408                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4409
4410        if calendar is not None:
4411            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4412
4413            # Saving calendar from Pandas DataFrame to XLSX sheet:
4414            if xlsx:
4415                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4416
4417                with pd.ExcelWriter(
4418                        path=xlsxCalendarFile,
4419                        date_format=TKS_DATE_FORMAT,
4420                        datetime_format=TKS_DATE_TIME_FORMAT,
4421                        mode="w",
4422                ) as writer:
4423                    humanReadable = calendar.copy(deep=True)
4424                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4425                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4426                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4427                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4428                    humanReadable.columns = colNames  # human-readable column names
4429
4430                    humanReadable.to_excel(
4431                        writer,
4432                        sheet_name="Bond payments calendar",
4433                        index=False,
4434                        encoding="UTF-8",
4435                        freeze_panes=(1, 2),
4436                    )  # saving as XLSX-file with freeze first row and column as headers
4437
4438                    del humanReadable  # release df in memory
4439
4440                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4441
4442        return calendar
4443
4444    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4445        """
4446        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4447        Also, creates Markdown file with calendar data, `calendar.md` by default.
4448
4449        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4450
4451        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4452                        extended information about bonds: main info, current prices, bond payment calendar,
4453                        coupon yields, current yields and some statistics etc.
4454                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4455        :param show: if `True` then also printing bonds payment calendar to the console,
4456                     otherwise save to file `calendarFile` only. `False` by default.
4457        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4458        :return: multilines text in Markdown format with bonds payment calendar as a table.
4459        """
4460        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4461            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4462
4463        infoText = "# Bond payments calendar\n\n"
4464
4465        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4466
4467        if not (calendar is None or calendar.empty):
4468            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4469
4470            info = [
4471                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4472                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4473                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4474            ]
4475
4476            newMonth = False
4477            notOneBond = calendar["figi"].nunique() > 1
4478            for i, bond in enumerate(calendar.iterrows()):
4479                if newMonth and notOneBond:
4480                    info.append(splitLine)
4481
4482                info.append(
4483                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4484                        "  √" if bond[1]["paid"] else "  —",
4485                        bond[1]["couponDate"].split("T")[0],
4486                        bond[1]["figi"],
4487                        bond[1]["ticker"],
4488                        bond[1]["couponNumber"],
4489                        "{} {}".format(
4490                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4491                            bond[1]["payCurrency"],
4492                        ),
4493                        bond[1]["couponType"],
4494                        bond[1]["couponPeriod"],
4495                        bond[1]["fixDate"].split("T")[0],
4496                    )
4497                )
4498
4499                if i < len(calendar.values) - 1:
4500                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4501                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4502                    newMonth = False if curDate.month == nextDate.month else True
4503
4504                else:
4505                    newMonth = False
4506
4507            infoText += "".join(info)
4508
4509            if show and not onlyFiles:
4510                uLogger.info("{}".format(infoText))
4511
4512            if self.calendarFile is not None and (show or onlyFiles):
4513                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4514                    fH.write(infoText)
4515
4516                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4517
4518                if self.useHTMLReports:
4519                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4520                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4521                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4522
4523                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4524
4525        else:
4526            infoText += "No data\n"
4527
4528        return infoText
4529
4530    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4531        """
4532        Method for parsing and show simple table with all available user accounts.
4533
4534        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4535
4536        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4537        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4538        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4539                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4540                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4541                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4542                                                        "closed": "—", "access": "Full access" }, ...}}`
4543        """
4544        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4545
4546        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4547        accounts = {
4548            item["id"]: {
4549                "type": TKS_ACCOUNT_TYPES[item["type"]],
4550                "name": item["name"],
4551                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4552                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4553                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4554                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4555            } for item in rawAccounts["accounts"]
4556        }
4557
4558        # Raw and parsed data with some fields replaced in "stat" section:
4559        view = {
4560            "rawAccounts": rawAccounts,
4561            "stat": accounts,
4562        }
4563
4564        # --- Prepare simple text table with only accounts data in human-readable format:
4565        if show or onlyFiles:
4566            info = [
4567                "# User accounts\n\n",
4568                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4569                "| Account ID   | Type                      | Status                    | Name                           |\n",
4570                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4571            ]
4572
4573            for account in view["stat"].keys():
4574                info.extend([
4575                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4576                        account,
4577                        view["stat"][account]["type"],
4578                        view["stat"][account]["status"],
4579                        view["stat"][account]["name"],
4580                    )
4581                ])
4582
4583            infoText = "".join(info)
4584
4585            if show and not onlyFiles:
4586                uLogger.info(infoText)
4587
4588            if self.userAccountsFile and (show or onlyFiles):
4589                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4590                    fH.write(infoText)
4591
4592                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4593
4594                if self.useHTMLReports:
4595                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4596                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4597                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4598
4599                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4600
4601        return view
4602
4603    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4604        """
4605        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4606
4607        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4608
4609        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4610        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4611        :return: dict with raw parsed data from server and some calculated statistics about it.
4612        """
4613        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4614        tmpTicker = self._ticker
4615        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4616        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4617        self._ticker = tmpTicker
4618
4619        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4620        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4621        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4622        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4623        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4624        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4625
4626        # This is dict with parsed common user data:
4627        userInfo = {
4628            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4629            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4630            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4631            "tariff": rawUserInfo["tariff"],
4632        }
4633
4634        # This is an array of dict with parsed margin statuses for every account IDs:
4635        margins = {}
4636        for accountId in accounts.keys():
4637            if rawMargins[accountId]:
4638                margins[accountId] = {
4639                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4640                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4641                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4642                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4643                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4644                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4645                    "missing": missing["volume"],
4646                }
4647
4648            else:
4649                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4650
4651        unary = {}  # unary-connection limits
4652        for item in rawTariffLimits["unaryLimits"]:
4653            if item["limitPerMinute"] in unary.keys():
4654                unary[item["limitPerMinute"]].extend(item["methods"])
4655
4656            else:
4657                unary[item["limitPerMinute"]] = item["methods"]
4658
4659        stream = {}  # stream-connection limits
4660        for item in rawTariffLimits["streamLimits"]:
4661            if item["limit"] in stream.keys():
4662                stream[item["limit"]].extend(item["streams"])
4663
4664            else:
4665                stream[item["limit"]] = item["streams"]
4666
4667        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4668        limits = {
4669            "unary": unary,
4670            "stream": stream,
4671        }
4672
4673        # Raw and parsed data as an output result:
4674        view = {
4675            "rawUserInfo": rawUserInfo,
4676            "rawAccounts": rawAccounts,
4677            "rawMargins": rawMargins,
4678            "rawTariffLimits": rawTariffLimits,
4679            "stat": {
4680                "overview": overview,
4681                "userInfo": userInfo,
4682                "accounts": accounts,
4683                "margins": margins,
4684                "limits": limits,
4685            },
4686        }
4687
4688        # --- Prepare text table with user information in human-readable format:
4689        if show or onlyFiles:
4690            info = [
4691                "# Full user information\n\n",
4692                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4693                "## Common information\n\n",
4694                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4695                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4696                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4697                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4698                "\n## User accounts\n\n",
4699            ]
4700
4701            for account in view["stat"]["accounts"].keys():
4702                info.extend([
4703                    "### ID: [{}]\n\n".format(account),
4704                    "| Parameters           | Values                                                       |\n",
4705                    "|----------------------|--------------------------------------------------------------|\n",
4706                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4707                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4708                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4709                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4710                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4711                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4712                ])
4713
4714                if margins[account]:
4715                    info.extend([
4716                        "| Margin status:       | Enabled                                                      |\n",
4717                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4718                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4719                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4720                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4721                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4722                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4723                    ])
4724
4725                else:
4726                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4727
4728            info.extend([
4729                "\n## Current user tariff limits\n",
4730                "\n### See also\n",
4731                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4732                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4733                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4734                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4735                "\n### Unary limits\n",
4736            ])
4737
4738            if unary:
4739                for key, values in sorted(unary.items()):
4740                    info.append("\n* Max requests per minute: {}\n".format(key))
4741
4742                    for value in values:
4743                        info.append("  - {}\n".format(value))
4744
4745            else:
4746                info.append("\nNot available\n")
4747
4748            info.append("\n### Stream limits\n")
4749
4750            if stream:
4751                for key, values in sorted(stream.items()):
4752                    info.append("\n* Max stream connections: {}\n".format(key))
4753
4754                    for value in values:
4755                        info.append("  - {}\n".format(value))
4756
4757            else:
4758                info.append("\nNot available\n")
4759
4760            infoText = "".join(info)
4761
4762            if show and not onlyFiles:
4763                uLogger.info(infoText)
4764
4765            if self.userInfoFile and (show or onlyFiles):
4766                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4767                    fH.write(infoText)
4768
4769                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4770
4771                if self.useHTMLReports:
4772                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4773                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4774                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4775
4776                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4777
4778        return view
4779
4780
4781class Args:
4782    """
4783    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4784    """
4785    def __init__(self, **kwargs):
4786        self.__dict__.update(kwargs)
4787
4788    def __getattr__(self, item):
4789        return None
4790
4791
4792def ParseArgs():
4793    """This function get and parse command line keys."""
4794    parser = ArgumentParser()  # command-line string parser
4795
4796    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4797    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4798
4799    # --- options:
4800
4801    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4802    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4803    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4804
4805    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4806    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4807
4808    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4809    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4810
4811    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4812    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4813
4814    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4815    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4816    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4817
4818    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4819    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4820    parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).")
4821
4822    # --- commands:
4823
4824    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4825
4826    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4827    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4828    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4829    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4830    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4831    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4832    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4833    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4834
4835    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4836    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4837    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4838    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4839    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4840    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4841
4842    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4843    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4844    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4845    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4846
4847    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4848    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4849    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4850
4851    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4852    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4853    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4854    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4855    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4856    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4857    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4858
4859    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4860    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4861    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4862    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4863    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4864
4865    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4866    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4867    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4868
4869    cmdArgs = parser.parse_args()
4870    return cmdArgs
4871
4872
4873def Main(**kwargs):
4874    """
4875    Main function for work with TKSBrokerAPI in the console.
4876
4877    See examples:
4878    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4879    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4880    """
4881    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4882
4883    if args.debug_level:
4884        uLogger.level = 10  # always debug level by default
4885        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4886
4887    exitCode = 0
4888    start = datetime.now(tzutc())
4889    uLogger.debug("=-" * 50)
4890    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4891        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4892        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4893    ))
4894
4895    # trying to calculate full current version:
4896    buildVersion = __version__
4897    try:
4898        v = version("tksbrokerapi")
4899        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4900
4901    except Exception:
4902        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4903
4904    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4905    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4906
4907    try:
4908        if args.version:
4909            print("TKSBrokerAPI {}".format(buildVersion))
4910            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4911
4912        else:
4913            # Init class for trading with Tinkoff Broker:
4914            trader = TinkoffBrokerServer(
4915                token=args.token,
4916                accountId=args.account_id,
4917                useCache=not args.no_cache,
4918            )
4919
4920            if args.tag is not None:
4921                trader.tag = args.tag  # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode
4922
4923            # --- set some options:
4924
4925            if args.more:
4926                trader.moreDebug = True
4927                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4928
4929            if args.html:
4930                trader.useHTMLReports = True
4931
4932            if args.ticker:
4933                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4934
4935                if ticker in trader.aliasesKeys:
4936                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4937
4938                else:
4939                    trader.ticker = ticker
4940
4941            if args.figi:
4942                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4943
4944            if args.depth is not None:
4945                trader.depth = args.depth
4946
4947            # --- do one command:
4948
4949            if args.list:
4950                if args.output is not None:
4951                    trader.instrumentsFile = args.output
4952
4953                trader.ShowInstrumentsInfo(show=True)
4954
4955            elif args.list_xlsx:
4956                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4957
4958            elif args.bonds_xlsx is not None:
4959                if args.output is not None:
4960                    trader.bondsXLSXFile = args.output
4961
4962                if len(args.bonds_xlsx) == 0:
4963                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4964
4965                else:
4966                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4967
4968            elif args.search:
4969                if args.output is not None:
4970                    trader.searchResultsFile = args.output
4971
4972                trader.SearchInstruments(pattern=args.search[0], show=True)
4973
4974            elif args.info:
4975                if not (args.ticker or args.figi):
4976                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4977                    raise Exception("Ticker or FIGI required")
4978
4979                if args.output is not None:
4980                    trader.infoFile = args.output
4981
4982                if args.ticker:
4983                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4984
4985                else:
4986                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4987
4988            elif args.calendar is not None:
4989                if args.output is not None:
4990                    trader.calendarFile = args.output
4991
4992                if len(args.calendar) == 0:
4993                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4994
4995                else:
4996                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4997
4998                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4999
5000            elif args.price:
5001                if not (args.ticker or args.figi):
5002                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5003                    raise Exception("Ticker or FIGI required")
5004
5005                trader.GetCurrentPrices(show=True)
5006
5007            elif args.prices is not None:
5008                if args.output is not None:
5009                    trader.pricesFile = args.output
5010
5011                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
5012
5013            elif args.overview:
5014                if args.output is not None:
5015                    trader.overviewFile = args.output
5016
5017                trader.Overview(show=True, details="full")
5018
5019            elif args.overview_digest:
5020                if args.output is not None:
5021                    trader.overviewDigestFile = args.output
5022
5023                trader.Overview(show=True, details="digest")
5024
5025            elif args.overview_positions:
5026                if args.output is not None:
5027                    trader.overviewPositionsFile = args.output
5028
5029                trader.Overview(show=True, details="positions")
5030
5031            elif args.overview_orders:
5032                if args.output is not None:
5033                    trader.overviewOrdersFile = args.output
5034
5035                trader.Overview(show=True, details="orders")
5036
5037            elif args.overview_analytics:
5038                if args.output is not None:
5039                    trader.overviewAnalyticsFile = args.output
5040
5041                trader.Overview(show=True, details="analytics")
5042
5043            elif args.overview_calendar:
5044                if args.output is not None:
5045                    trader.overviewAnalyticsFile = args.output
5046
5047                trader.Overview(show=True, details="calendar")
5048
5049            elif args.deals is not None:
5050                if args.output is not None:
5051                    trader.reportFile = args.output
5052
5053                if 0 <= len(args.deals) < 3:
5054                    trader.Deals(
5055                        start=args.deals[0] if len(args.deals) >= 1 else None,
5056                        end=args.deals[1] if len(args.deals) == 2 else None,
5057                        show=True,  # Always show deals report in console
5058                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5059                    )
5060
5061                else:
5062                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5063                    raise Exception("Incorrect value")
5064
5065            elif args.history is not None:
5066                if args.output is not None:
5067                    trader.historyFile = args.output
5068
5069                if 0 <= len(args.history) < 3:
5070                    dataReceived = trader.History(
5071                        start=args.history[0] if len(args.history) >= 1 else None,
5072                        end=args.history[1] if len(args.history) == 2 else None,
5073                        interval="hour" if args.interval is None or not args.interval else args.interval,
5074                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5075                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5076                        show=True,  # shows all downloaded candles in console
5077                    )
5078
5079                    if args.render_chart is not None and dataReceived is not None:
5080                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5081
5082                        trader.ShowHistoryChart(
5083                            candles=dataReceived,
5084                            interact=iChart,
5085                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5086                        )
5087
5088                else:
5089                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5090                    raise Exception("Incorrect value")
5091
5092            elif args.load_history is not None:
5093                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5094
5095                if args.render_chart is not None and histData is not None:
5096                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5097                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5098
5099                    trader.ShowHistoryChart(
5100                        candles=histData,
5101                        interact=iChart,
5102                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5103                    )
5104
5105            elif args.trade is not None:
5106                if 1 <= len(args.trade) <= 5:
5107                    trader.Trade(
5108                        operation=args.trade[0],
5109                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5110                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5111                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5112                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5113                    )
5114
5115                else:
5116                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5117
5118            elif args.buy is not None:
5119                if 0 <= len(args.buy) <= 4:
5120                    trader.Buy(
5121                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5122                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5123                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5124                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5125                    )
5126
5127                else:
5128                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5129
5130            elif args.sell is not None:
5131                if 0 <= len(args.sell) <= 4:
5132                    trader.Sell(
5133                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5134                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5135                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5136                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5137                    )
5138
5139                else:
5140                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5141
5142            elif args.order:
5143                if 4 <= len(args.order) <= 7:
5144                    trader.Order(
5145                        operation=args.order[0],
5146                        orderType=args.order[1],
5147                        lots=int(args.order[2]),
5148                        targetPrice=float(args.order[3]),
5149                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5150                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5151                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5152                    )
5153
5154                else:
5155                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5156
5157            elif args.buy_limit:
5158                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5159
5160            elif args.sell_limit:
5161                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5162
5163            elif args.buy_stop:
5164                if 2 <= len(args.buy_stop) <= 7:
5165                    trader.BuyStop(
5166                        lots=int(args.buy_stop[0]),
5167                        targetPrice=float(args.buy_stop[1]),
5168                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5169                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5170                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5171                    )
5172
5173                else:
5174                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5175
5176            elif args.sell_stop:
5177                if 2 <= len(args.sell_stop) <= 7:
5178                    trader.SellStop(
5179                        lots=int(args.sell_stop[0]),
5180                        targetPrice=float(args.sell_stop[1]),
5181                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5182                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5183                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5184                    )
5185
5186                else:
5187                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5188
5189            # elif args.buy_order_grid is not None:
5190            #     # update order grid work with api v2
5191            #     if len(args.buy_order_grid) == 2:
5192            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5193            #
5194            #         for order in orderParams:
5195            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5196            #
5197            #     else:
5198            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5199            #
5200            # elif args.sell_order_grid is not None:
5201            #     # update order grid work with api v2
5202            #     if len(args.sell_order_grid) >= 2:
5203            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5204            #
5205            #         for order in orderParams:
5206            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5207            #
5208            #     else:
5209            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5210
5211            elif args.close_order is not None:
5212                trader.CloseOrders(args.close_order)  # close only one order
5213
5214            elif args.close_orders is not None:
5215                trader.CloseOrders(args.close_orders)  # close list of orders
5216
5217            elif args.close_trade:
5218                if not (args.ticker or args.figi):
5219                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5220                    raise Exception("Ticker or FIGI required")
5221
5222                if args.ticker:
5223                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5224
5225                else:
5226                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5227
5228            elif args.close_trades is not None:
5229                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5230
5231            elif args.close_all is not None:
5232                if args.ticker:
5233                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5234
5235                elif args.figi:
5236                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5237
5238                else:
5239                    trader.CloseAll(*args.close_all)
5240
5241            elif args.limits:
5242                if args.output is not None:
5243                    trader.withdrawalLimitsFile = args.output
5244
5245                trader.OverviewLimits(show=True)
5246
5247            elif args.user_info:
5248                if args.output is not None:
5249                    trader.userInfoFile = args.output
5250
5251                trader.OverviewUserInfo(show=True)
5252
5253            elif args.account:
5254                if args.output is not None:
5255                    trader.userAccountsFile = args.output
5256
5257                trader.OverviewAccounts(show=True)
5258
5259            else:
5260                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5261                raise Exception("There is no command to execute")
5262
5263    except Exception:
5264        trace = tb.format_exc()
5265        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5266            if e in trace:
5267                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5268                break
5269
5270        uLogger.debug(trace)
5271        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5272        exitCode = 255  # an error occurred, must be open a ticket for this issue
5273
5274    finally:
5275        finish = datetime.now(tzutc())
5276
5277        if exitCode == 0:
5278            if args.more:
5279                uLogger.debug("All operations were finished success (summary code is 0).")
5280
5281        else:
5282            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5283                os.path.abspath(uLog.defaultLogFile), exitCode,
5284            ))
5285
5286        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5287        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5288            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5289            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5290        ))
5291        uLogger.debug("=-" * 50)
5292
5293        if not kwargs:
5294            sys.exit(exitCode)
5295
5296        else:
5297            return exitCode
5298
5299
5300if __name__ == "__main__":
5301    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self._tag = ""
 130        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 131
 132        self.__lock = Lock()  # initialize multiprocessing mutex lock
 133
 134        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
 135
 136        self.aliases = TKS_TICKER_ALIASES
 137        """Some aliases instead official tickers.
 138
 139        See also: `TKSEnums.TKS_TICKER_ALIASES`
 140        """
 141
 142        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 143
 144        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 145
 146        self._ticker = ""
 147        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 148
 149        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 150        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 151
 152        See also: `SearchByTicker()`, `SearchInstruments()`.
 153        """
 154
 155        self._figi = ""
 156        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 157
 158        See also: `SearchByFIGI()`, `SearchInstruments()`.
 159        """
 160
 161        self.depth = 1
 162        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 163
 164        See also: `GetCurrentPrices()`.
 165        """
 166
 167        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 168        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 169
 170        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 171        """
 172
 173        uLogger.debug("Broker API server: {}".format(self.server))
 174
 175        self.timeout = 15
 176        """Server operations timeout in seconds. Default: `15`.
 177
 178        See also: `SendAPIRequest()`.
 179        """
 180
 181        self.headers = {
 182            "Content-Type": "application/json",
 183            "accept": "application/json",
 184            "Authorization": "Bearer {}".format(self.token),
 185            "x-app-name": "Tim55667757.TKSBrokerAPI",
 186        }
 187        """
 188        Headers which send in every request to broker server. Please, do not change it!
 189        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
 190
 191        See also: `SendAPIRequest()`.
 192        """
 193
 194        self.body = None
 195        """Request body which send to broker server. Default: `None`.
 196
 197        See also: `SendAPIRequest()`.
 198        """
 199
 200        self.moreDebug = False
 201        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 202
 203        self.useHTMLReports = False
 204        """
 205        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 206        
 207        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 208        """
 209
 210        self.historyFile = None
 211        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 212
 213        See also: `History()`.
 214        """
 215
 216        self.htmlHistoryFile = "index.html"
 217        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 218
 219        See also: `ShowHistoryChart()`.
 220        """
 221
 222        self.instrumentsFile = "instruments.md"
 223        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 224
 225        See also: `ShowInstrumentsInfo()`.
 226        """
 227
 228        self.searchResultsFile = "search-results.md"
 229        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 230
 231        See also: `SearchInstruments()`.
 232        """
 233
 234        self.pricesFile = "prices.md"
 235        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 236
 237        See also: `GetListOfPrices()`.
 238        """
 239
 240        self.infoFile = "info.md"
 241        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 242
 243        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 244        """
 245
 246        self.bondsXLSXFile = "ext-bonds.xlsx"
 247        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 248        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 249
 250        See also: `ExtendBondsData()`.
 251        """
 252
 253        self.calendarFile = "calendar.md"
 254        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 255        
 256        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 257
 258        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 259        """
 260
 261        self.overviewFile = "overview.md"
 262        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 263
 264        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 265        """
 266
 267        self.overviewDigestFile = "overview-digest.md"
 268        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 269
 270        See also: `Overview()` with parameter `details="digest"`.
 271        """
 272
 273        self.overviewPositionsFile = "overview-positions.md"
 274        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 275
 276        See also: `Overview()` with parameter `details="positions"`.
 277        """
 278
 279        self.overviewOrdersFile = "overview-orders.md"
 280        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 281
 282        See also: `Overview()` with parameter `details="orders"`.
 283        """
 284
 285        self.overviewAnalyticsFile = "overview-analytics.md"
 286        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 287
 288        See also: `Overview()` with parameter `details="analytics"`.
 289        """
 290
 291        self.overviewBondsCalendarFile = "overview-calendar.md"
 292        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 293
 294        See also: `Overview()` with parameter `details="calendar"`.
 295        """
 296
 297        self.reportFile = "deals.md"
 298        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 299
 300        See also: `Deals()`.
 301        """
 302
 303        self.withdrawalLimitsFile = "limits.md"
 304        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 305
 306        See also: `OverviewLimits()` and `RequestLimits()`.
 307        """
 308
 309        self.userInfoFile = "user-info.md"
 310        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 311
 312        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 313        """
 314
 315        self.userAccountsFile = "accounts.md"
 316        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 317
 318        See also: `OverviewAccounts()`, `RequestAccounts()`.
 319        """
 320
 321        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 322        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 323
 324        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 325
 326        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 327        """
 328
 329        self.iList = None  # init iList for raw instruments data
 330        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 331        
 332        See also: `Listing()`, `DumpInstruments()`.
 333        """
 334
 335        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 336        if useCache:
 337            if os.path.exists(self.iListDumpFile):
 338                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 339                curTime = datetime.now(tzutc())
 340
 341                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 342                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 343
 344                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 345
 346                else:
 347                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 348
 349                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 350                        os.path.abspath(self.iListDumpFile),
 351                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 352                    ))
 353
 354            else:
 355                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 356                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 357
 358        else:
 359            self.iList = self.Listing()  # request new raw instruments data from broker server
 360            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 361
 362        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 363        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 364
 365        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 366        """
 367
 368    @property
 369    def tag(self) -> str:
 370        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 371        return self._tag
 372
 373    @tag.setter
 374    def tag(self, value):
 375        """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 376        self._tag = str(value)
 377
 378        if self._tag:
 379            for handler in uLogger.handlers:
 380                handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag)))
 381
 382            uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag))
 383
 384        else:
 385            for handler in uLogger.handlers:
 386                handler.setFormatter(uLog.logging.Formatter(uLog.formatString))
 387
 388            uLogger.debug("Default logger format is used")
 389
 390    @property
 391    def ticker(self) -> str:
 392        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 393
 394        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 395        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 396
 397        See also: `SearchByTicker()`, `SearchInstruments()`.
 398        """
 399        return self._ticker
 400
 401    @ticker.setter
 402    def ticker(self, value):
 403        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 404
 405        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 406        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 407
 408        See also: `SearchByTicker()`, `SearchInstruments()`.
 409        """
 410        self._ticker = str(value).upper()  # Tickers may be upper case only
 411
 412    @property
 413    def figi(self) -> str:
 414        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 415
 416        See also: `SearchByFIGI()`, `SearchInstruments()`.
 417        """
 418        return self._figi
 419
 420    @figi.setter
 421    def figi(self, value):
 422        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 423
 424        See also: `SearchByFIGI()`, `SearchInstruments()`.
 425        """
 426        self._figi = str(value).upper()  # FIGI may be upper case only
 427
 428    def _ParseJSON(self, rawData="{}") -> dict:
 429        """
 430        Parse JSON from response string.
 431
 432        :param rawData: this is a string with JSON-formatted text.
 433        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 434        """
 435        try:
 436            responseJSON = json.loads(rawData) if rawData else {}
 437
 438            if self.moreDebug:
 439                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 440
 441            return responseJSON
 442
 443        except Exception as e:
 444            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 445
 446            return {}
 447
 448    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 449        """
 450        Send GET or POST request to broker server and receive JSON object.
 451
 452        self.header: must be defining with dictionary of headers.
 453        self.body: if define then used as request body. None by default.
 454        self.timeout: global request timeout, 15 seconds by default.
 455        :param url: url with REST request.
 456        :param reqType: send "GET" or "POST" request. "GET" by default.
 457        :param retry: how many times retry after first request if an 5xx server errors occurred.
 458        :param pause: sleep time in seconds between retries.
 459        :return: response JSON (dictionary) from broker.
 460        """
 461        if reqType.upper() not in ("GET", "POST"):
 462            uLogger.error("You can define request type: `GET` or `POST`!")
 463            raise Exception("Incorrect value")
 464
 465        if self.moreDebug:
 466            uLogger.debug("Request parameters:")
 467            uLogger.debug("    - REST API URL: {}".format(url))
 468            uLogger.debug("    - request type: {}".format(reqType))
 469            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 470            uLogger.debug("    - body:\n{}".format(self.body))
 471
 472        # fast hack to avoid all operations with some tickers/FIGI
 473        responseJSON = {}
 474        oK = True
 475        for item in self.exclude:
 476            if item in url:
 477                if self.moreDebug:
 478                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 479
 480                oK = False
 481                break
 482
 483        if oK:
 484            with self.__lock:  # acquire the mutex lock
 485                counter = 0
 486                response = None
 487                errMsg = ""
 488
 489                while not response and counter <= retry:
 490                    if reqType == "GET":
 491                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 492
 493                    if reqType == "POST":
 494                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 495
 496                    if self.moreDebug:
 497                        uLogger.debug("Response:")
 498                        uLogger.debug("    - status code: {}".format(response.status_code))
 499                        uLogger.debug("    - reason: {}".format(response.reason))
 500                        uLogger.debug("    - body length: {}".format(len(response.text)))
 501                        uLogger.debug("    - headers:\n{}".format(response.headers))
 502
 503                    # Server returns some headers:
 504                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 505                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 506                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 507                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 508                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 509                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 510                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 511                        sleep(rateLimitWait)
 512
 513                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 514                    if 400 <= response.status_code < 500:
 515                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 516                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 517
 518                        if "code" in response.text and "message" in response.text:
 519                            msgDict = self._ParseJSON(rawData=response.text)
 520                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 521
 522                        counter = retry + 1  # do not retry for 4xx errors
 523
 524                    if 500 <= response.status_code < 600:
 525                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 526                        uLogger.debug("    - not oK, {}".format(errMsg))
 527
 528                        if "code" in response.text and "message" in response.text:
 529                            errMsgDict = self._ParseJSON(rawData=response.text)
 530                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 531
 532                        counter += 1
 533
 534                        if counter <= retry:
 535                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 536                            sleep(pause)
 537
 538                responseJSON = self._ParseJSON(rawData=response.text)
 539
 540                if errMsg:
 541                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 542                    uLogger.error("    - not oK, {}".format(errMsg))
 543
 544        return responseJSON
 545
 546    def _IUpdater(self, iType: str) -> tuple:
 547        """
 548        Request instrument by type from server. See available API methods for instruments:
 549        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 550        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 551        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 552        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 553        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 554
 555        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 556        :return: tuple with iType name and list of available instruments of current type for defined user token.
 557        """
 558        result = []
 559
 560        if iType in TKS_INSTRUMENTS:
 561            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 562
 563            # all instruments have the same body in API v2 requests:
 564            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 565            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 566            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 567
 568        return iType, result
 569
 570    def _IWrapper(self, kwargs):
 571        """
 572        Wrapper runs instrument's update method `_IUpdater()`.
 573        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 574        """
 575        return self._IUpdater(**kwargs)
 576
 577    def Listing(self) -> dict:
 578        """
 579        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 580
 581        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 582        """
 583        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 584        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 585
 586        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 587        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 588        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 589
 590        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 591        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 592        poolUpdater.close()  # close the thread pool
 593        poolUpdater.join()  # wait a moment until all data returns from threads
 594
 595        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 596        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 597        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 598
 599        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 600        for iType in iList.keys():
 601            for ticker in iList[iType]:
 602                iList[iType][ticker]["type"] = iType
 603
 604                if "minPriceIncrement" in iList[iType][ticker].keys():
 605                    iList[iType][ticker]["step"] = NanoToFloat(
 606                        iList[iType][ticker]["minPriceIncrement"]["units"],
 607                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 608                    )
 609
 610                else:
 611                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 612
 613        return iList
 614
 615    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 616        """
 617        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 618
 619        See also: `DumpInstruments()`, `Listing()`.
 620
 621        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 622                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 623        """
 624        if self.iListDumpFile is None or not self.iListDumpFile:
 625            uLogger.error("Output name of dump file must be defined!")
 626            raise Exception("Filename required")
 627
 628        if not self.iList or forceUpdate:
 629            self.iList = self.Listing()
 630
 631        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 632
 633        # Save as XLSX with separated sheets for every type of instruments:
 634        with pd.ExcelWriter(
 635                path=xlsxDumpFile,
 636                date_format=TKS_DATE_FORMAT,
 637                datetime_format=TKS_DATE_TIME_FORMAT,
 638                mode="w",
 639        ) as writer:
 640            for iType in TKS_INSTRUMENTS:
 641                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 642                df = df[sorted(df)]  # sorted by column names
 643                df = df.applymap(
 644                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 645                    na_action="ignore",
 646                )  # converting numbers from nano-type to float in every cell
 647                df.to_excel(
 648                    writer,
 649                    sheet_name=iType,
 650                    encoding="UTF-8",
 651                    freeze_panes=(1, 1),
 652                )  # saving as XLSX-file with freeze first row and column as headers
 653
 654        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 655
 656    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 657        """
 658        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 659        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 660
 661        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 662
 663        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 664                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 665        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 666        """
 667        if self.iListDumpFile is None or not self.iListDumpFile:
 668            uLogger.error("Output name of dump file must be defined!")
 669            raise Exception("Filename required")
 670
 671        if not self.iList or forceUpdate:
 672            self.iList = self.Listing()
 673
 674        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 675        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 676            fH.write(jsonDump)
 677
 678        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 679
 680        return jsonDump
 681
 682    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 683        """
 684        Show information about one instrument defined by json data and prints it in Markdown format.
 685
 686        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 687
 688        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 689        :param show: if `True` then also printing information about instrument and its current price.
 690        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 691        :return: multilines text in Markdown format with information about one instrument.
 692        """
 693        splitLine = "|                                                             |                                                        |\n"
 694        infoText = ""
 695
 696        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 697            info = [
 698                "# Main information\n\n",
 699                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 700                "| Parameters                                                  | Values                                                 |\n",
 701                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 702                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 703                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 704            ]
 705
 706            if "sector" in iJSON.keys() and iJSON["sector"]:
 707                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 708
 709            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 710                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 711
 712            info.extend([
 713                splitLine,
 714                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 715                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 716            ])
 717
 718            if "isin" in iJSON.keys() and iJSON["isin"]:
 719                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 720
 721            if "classCode" in iJSON.keys():
 722                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 723
 724            info.extend([
 725                splitLine,
 726                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 727                splitLine,
 728                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 729                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 730                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 731            ])
 732
 733            if iJSON["figi"]:
 734                self._figi = iJSON["figi"]
 735                iJSON = iJSON | self.RequestTradingStatus()
 736
 737                info.extend([
 738                    splitLine,
 739                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 740                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 741                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 742                ])
 743
 744            info.append(splitLine)
 745
 746            if "type" in iJSON.keys() and iJSON["type"]:
 747                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 748
 749                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 750                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 751
 752            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 753                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 754
 755            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 756                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 757
 758            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 759                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 760
 761            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 762                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 763
 764            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 765                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 766
 767            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 768                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 769
 770            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 771                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 772
 773            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 774                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 775
 776            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 777                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 778
 779            if "currency" in iJSON.keys():
 780                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 781
 782            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 783                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 784
 785            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 786                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 787
 788            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 789                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 790
 791            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 792                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 793
 794            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 795                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 796
 797            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 798                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 799
 800            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 801                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 802
 803            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 804                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 805
 806            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 807                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 808
 809            iExt = None
 810            if iJSON["type"] == "Bonds":
 811                info.extend([
 812                    splitLine,
 813                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 814                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 815                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 816                        iJSON["nominal"]["currency"],
 817                    )),
 818                ])
 819
 820                if "floatingCouponFlag" in iJSON.keys():
 821                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 822
 823                if "amortizationFlag" in iJSON.keys():
 824                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 825
 826                info.append(splitLine)
 827
 828                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 829                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 830
 831                if iJSON["figi"]:
 832                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 833
 834                    info.extend([
 835                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 836                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 837                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 838                    ])
 839
 840                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 841                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 842                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 843                        iJSON["aciValue"]["currency"]
 844                    )))
 845
 846            if "currentPrice" in iJSON.keys():
 847                info.append(splitLine)
 848
 849                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 850                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 851
 852                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 853                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 854                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 855                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 856                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 857
 858                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 859                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 860
 861                info.extend([
 862                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 863                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 864                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 865                    )),
 866                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 867                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 868                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 869                    )),
 870                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 871                        "{:.2f}%{}".format(
 872                            iJSON["currentPrice"]["changes"],
 873                            " ({}{:.2f} {})".format(
 874                                "+" if bondChangesDelta > 0 else "",
 875                                bondChangesDelta,
 876                                aciCurrency
 877                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 878                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 879                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 880                                currency
 881                            ),
 882                        )
 883                    ),
 884                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 885                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 886                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 887                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 888                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 889                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 890                    )),
 891                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 892                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 893                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 894                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 896                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 897                    )),
 898                ])
 899
 900            if "lot" in iJSON.keys():
 901                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 902
 903            if "step" in iJSON.keys() and iJSON["step"] != 0:
 904                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 905
 906            # Add bond payment calendar:
 907            if iJSON["type"] == "Bonds":
 908                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 909                info.extend(["\n#", strCalendar])
 910
 911            infoText += "".join(info)
 912
 913            if show and not onlyFiles:
 914                uLogger.info("{}".format(infoText))
 915
 916            if self.infoFile is not None and (show or onlyFiles):
 917                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 918                    fH.write(infoText)
 919
 920                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 921
 922                if self.useHTMLReports:
 923                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 924                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 925                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 926
 927                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 928
 929        return infoText
 930
 931    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 932        """
 933        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 934
 935        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 936        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 937        :return: JSON formatted data with information about instrument.
 938        """
 939        tickerJSON = {}
 940        if self.moreDebug:
 941            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 942
 943        if not self._ticker:
 944            uLogger.warning("self._ticker variable is not be empty!")
 945
 946        else:
 947            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 948                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 949                raise Exception("Instrument not allowed")
 950
 951            if not self.iList:
 952                self.iList = self.Listing()
 953
 954            if self._ticker in self.iList["Shares"].keys():
 955                tickerJSON = self.iList["Shares"][self._ticker]
 956                if self.moreDebug:
 957                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 958
 959            elif self._ticker in self.iList["Currencies"].keys():
 960                tickerJSON = self.iList["Currencies"][self._ticker]
 961                if self.moreDebug:
 962                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 963
 964            elif self._ticker in self.iList["Bonds"].keys():
 965                tickerJSON = self.iList["Bonds"][self._ticker]
 966                if self.moreDebug:
 967                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 968
 969            elif self._ticker in self.iList["Etfs"].keys():
 970                tickerJSON = self.iList["Etfs"][self._ticker]
 971                if self.moreDebug:
 972                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 973
 974            elif self._ticker in self.iList["Futures"].keys():
 975                tickerJSON = self.iList["Futures"][self._ticker]
 976                if self.moreDebug:
 977                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 978
 979        if tickerJSON:
 980            self._figi = tickerJSON["figi"]
 981
 982            if requestPrice:
 983                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 984
 985                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 986                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 987
 988                else:
 989                    tickerJSON["currentPrice"]["changes"] = 0
 990
 991            if show:
 992                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 993
 994        else:
 995            if show:
 996                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 997
 998        return tickerJSON
 999
1000    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1001        """
1002        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1003
1004        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1005        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1006        :return: JSON formatted data with information about instrument.
1007        """
1008        figiJSON = {}
1009        if self.moreDebug:
1010            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1011
1012        if not self._figi:
1013            uLogger.warning("self._figi variable is not be empty!")
1014
1015        else:
1016            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1017                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1018                raise Exception("Instrument not allowed")
1019
1020            if not self.iList:
1021                self.iList = self.Listing()
1022
1023            for item in self.iList["Shares"].keys():
1024                if self._figi == self.iList["Shares"][item]["figi"]:
1025                    figiJSON = self.iList["Shares"][item]
1026
1027                    if self.moreDebug:
1028                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1029
1030                    break
1031
1032            if not figiJSON:
1033                for item in self.iList["Currencies"].keys():
1034                    if self._figi == self.iList["Currencies"][item]["figi"]:
1035                        figiJSON = self.iList["Currencies"][item]
1036
1037                        if self.moreDebug:
1038                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1039
1040                        break
1041
1042            if not figiJSON:
1043                for item in self.iList["Bonds"].keys():
1044                    if self._figi == self.iList["Bonds"][item]["figi"]:
1045                        figiJSON = self.iList["Bonds"][item]
1046
1047                        if self.moreDebug:
1048                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1049
1050                        break
1051
1052            if not figiJSON:
1053                for item in self.iList["Etfs"].keys():
1054                    if self._figi == self.iList["Etfs"][item]["figi"]:
1055                        figiJSON = self.iList["Etfs"][item]
1056
1057                        if self.moreDebug:
1058                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1059
1060                        break
1061
1062            if not figiJSON:
1063                for item in self.iList["Futures"].keys():
1064                    if self._figi == self.iList["Futures"][item]["figi"]:
1065                        figiJSON = self.iList["Futures"][item]
1066
1067                        if self.moreDebug:
1068                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1069
1070                        break
1071
1072        if figiJSON:
1073            self._figi = figiJSON["figi"]
1074            self._ticker = figiJSON["ticker"]
1075
1076            if requestPrice:
1077                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1078
1079                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1080                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1081
1082                else:
1083                    figiJSON["currentPrice"]["changes"] = 0
1084
1085            if show:
1086                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1087
1088        else:
1089            if show:
1090                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1091
1092        return figiJSON
1093
1094    def GetCurrentPrices(self, show: bool = True) -> dict:
1095        """
1096        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1097        `{"buy": [{"price": 1243.8, "quantity": 193},
1098                  {"price": 1244.0, "quantity": 168},
1099                  {"price": 1244.8, "quantity": 5},
1100                  {"price": 1245.0, "quantity": 61},
1101                  {"price": 1245.4, "quantity": 60}],
1102          "sell": [{"price": 1243.6, "quantity": 8},
1103                   {"price": 1242.6, "quantity": 10},
1104                   {"price": 1242.4, "quantity": 18},
1105                   {"price": 1242.2, "quantity": 50},
1106                   {"price": 1242.0, "quantity": 113}],
1107          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1108        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1109        - sell: list of dicts with Buyers prices,
1110            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1111            - quantity: volume value by current price in lots,
1112        - limitUp: current trade session limit price, maximum,
1113        - limitDown: current trade session limit price, minimum,
1114        - lastPrice: last deal price of the instrument,
1115        - closePrice: previous trade session close price of the instrument.
1116
1117        See also: `SearchByTicker()` and `SearchByFIGI()`.
1118        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1119        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1120
1121        :param show: if `True` then print DOM to log and console.
1122        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1123                 If an error occurred then returns an empty record:
1124                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1125        """
1126        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1127
1128        if self.depth < 1:
1129            uLogger.error("Depth of Market (DOM) must be >=1!")
1130            raise Exception("Incorrect value")
1131
1132        if not (self._ticker or self._figi):
1133            uLogger.error("self._ticker or self._figi variables must be defined!")
1134            raise Exception("Ticker or FIGI required")
1135
1136        if self._ticker and not self._figi:
1137            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1138            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1139
1140        if not self._ticker and self._figi:
1141            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1142            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1143
1144        if not self._figi:
1145            uLogger.error("FIGI is not defined!")
1146            raise Exception("Ticker or FIGI required")
1147
1148        else:
1149            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1150
1151            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1152            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1153            self.body = str({"figi": self._figi, "depth": self.depth})
1154            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1155
1156            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1157                # list of dicts with sellers orders:
1158                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1159
1160                # list of dicts with buyers orders:
1161                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1162
1163                # max price of instrument at this time:
1164                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1165
1166                # min price of instrument at this time:
1167                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1168
1169                # last price of deal with instrument:
1170                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1171
1172                # last close price of instrument:
1173                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1174
1175            else:
1176                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1177                uLogger.debug("Server response: {}".format(pricesResponse))
1178
1179            if show:
1180                if prices["buy"] or prices["sell"]:
1181                    info = [
1182                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1183                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1184                            self._ticker,
1185                            self._figi,
1186                            self.depth,
1187                        ),
1188                        "-" * 60, "\n",
1189                        "             Orders of Buyers | Orders of Sellers\n",
1190                        "-" * 60, "\n",
1191                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1192                        "-" * 60, "\n",
1193                    ]
1194
1195                    if not prices["buy"]:
1196                        info.append("                              | No orders!\n")
1197                        sumBuy = 0
1198
1199                    else:
1200                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1201                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1202                        for item in maxMinSorted:
1203                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1204
1205                    if not prices["sell"]:
1206                        info.append("No orders!                    |\n")
1207                        sumSell = 0
1208
1209                    else:
1210                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1211                        for item in prices["sell"]:
1212                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1213
1214                    info.extend([
1215                        "-" * 60, "\n",
1216                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1217                        "-" * 60, "\n",
1218                    ])
1219
1220                    infoText = "".join(info)
1221
1222                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1223
1224                else:
1225                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1226
1227        return prices
1228
1229    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1230        """
1231        This method get and show information about all available broker instruments for current user account.
1232        If `instrumentsFile` string is not empty then also save information to this file.
1233
1234        :param show: if `True` then print results to console, if `False` — print only to file.
1235        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1236        :return: multi-lines string with all available broker instruments.
1237        """
1238        if not self.iList:
1239            self.iList = self.Listing()
1240
1241        info = [
1242            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1243            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1244        ]
1245
1246        # add instruments count by type:
1247        for iType in self.iList.keys():
1248            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1249
1250        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1251        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1252
1253        # generating info tables with all instruments by type:
1254        for iType in self.iList.keys():
1255            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1256
1257            for instrument in self.iList[iType].keys():
1258                iName = self.iList[iType][instrument]["name"]  # instrument's name
1259                if len(iName) > 57:
1260                    iName = "{}...".format(iName[:54])  # right trim for a long string
1261
1262                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1263                    self.iList[iType][instrument]["ticker"],
1264                    iName,
1265                    self.iList[iType][instrument]["figi"],
1266                    self.iList[iType][instrument]["currency"],
1267                    self.iList[iType][instrument]["lot"],
1268                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1269                ))
1270
1271        infoText = "".join(info)
1272
1273        if show and not onlyFiles:
1274            uLogger.info(infoText)
1275
1276        if self.instrumentsFile and (show or onlyFiles):
1277            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1278                fH.write(infoText)
1279
1280            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1281
1282            if self.useHTMLReports:
1283                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1284                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1285                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1286
1287                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1288
1289        return infoText
1290
1291    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1292        """
1293        This method search and show information about instruments by part of its ticker, FIGI or name.
1294        If `searchResultsFile` string is not empty then also save information to this file.
1295
1296        :param pattern: string with part of ticker, FIGI or instrument's name.
1297        :param show: if `True` then print results to console, if `False` — return list of result only.
1298        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1299        :return: list of dictionaries with all found instruments.
1300        """
1301        if not self.iList:
1302            self.iList = self.Listing()
1303
1304        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1305        compiledPattern = re.compile(pattern, re.IGNORECASE)
1306
1307        for iType in self.iList:
1308            for instrument in self.iList[iType].values():
1309                searchResult = compiledPattern.search(" ".join(
1310                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1311                ))
1312
1313                if searchResult:
1314                    searchResults[iType][instrument["ticker"]] = instrument
1315
1316        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1317        info = [
1318            "# Search results\n\n",
1319            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1320            "* **Search pattern:** [{}]\n".format(pattern),
1321            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1322            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1323        ]
1324        infoShort = info[:]
1325
1326        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1327        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1328        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1329
1330        if resultsLen == 0:
1331            info.append("\nNo results\n")
1332            infoShort.append("\nNo results\n")
1333            uLogger.warning("No results. Try changing your search pattern.")
1334
1335        else:
1336            for iType in searchResults:
1337                iTypeValuesCount = len(searchResults[iType].values())
1338                if iTypeValuesCount > 0:
1339                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1340                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1341
1342                    for instrument in searchResults[iType].values():
1343                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1344                            instrument["type"],
1345                            instrument["ticker"],
1346                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1347                            instrument["figi"],
1348                        ))
1349
1350                    if iTypeValuesCount <= 5:
1351                        infoShort.extend(info[-iTypeValuesCount:])
1352
1353                    else:
1354                        infoShort.extend(info[-5:])
1355                        infoShort.append(skippedLine)
1356
1357        infoText = "".join(info)
1358        infoTextShort = "".join(infoShort)
1359
1360        if show and not onlyFiles:
1361            uLogger.info(infoTextShort)
1362            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1363
1364        if self.searchResultsFile and (show or onlyFiles):
1365            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1366                fH.write(infoText)
1367
1368            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1369
1370            if self.useHTMLReports:
1371                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1372                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1373                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1374
1375                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1376
1377        return searchResults
1378
1379    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1380        """
1381        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1382
1383        :param instruments: list of strings with tickers or FIGIs.
1384        :return: list with unique instrument FIGIs only.
1385        """
1386        requestedInstruments = []
1387        for iName in instruments:
1388            if iName not in self.aliases.keys():
1389                if iName not in requestedInstruments:
1390                    requestedInstruments.append(iName)
1391
1392            else:
1393                if iName not in requestedInstruments:
1394                    if self.aliases[iName] not in requestedInstruments:
1395                        requestedInstruments.append(self.aliases[iName])
1396
1397        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1398
1399        onlyUniqueFIGIs = []
1400        for iName in requestedInstruments:
1401            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1402                continue
1403
1404            self._ticker = iName
1405            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1406
1407            if not iData:
1408                self._ticker = ""
1409                self._figi = iName
1410
1411                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1412
1413                if not iData:
1414                    self._figi = ""
1415                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1416
1417            if iData and iData["figi"] not in onlyUniqueFIGIs:
1418                onlyUniqueFIGIs.append(iData["figi"])
1419
1420        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1421
1422        return onlyUniqueFIGIs
1423
1424    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1425        """
1426        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1427
1428        See limits: https://tinkoff.github.io/investAPI/limits/
1429
1430        If `pricesFile` string is not empty then also save information to this file.
1431
1432        :param instruments: list of strings with tickers or FIGIs.
1433        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1434        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1435        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1436                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1437        """
1438        if instruments is None or not instruments:
1439            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1440            raise Exception("Ticker or FIGI required")
1441
1442        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1443
1444        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1445
1446        iList = []  # trying to get info and current prices about all unique instruments:
1447        for self._figi in onlyUniqueFIGIs:
1448            iData = self.SearchByFIGI(requestPrice=True, show=False)
1449            iList.append(iData)
1450
1451        self.ShowListOfPrices(iList, show, onlyFiles)
1452
1453        return iList
1454
1455    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1456        """
1457        Show table contains current prices of given instruments.
1458
1459        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1460                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1461        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1462        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1463        :return: multilines text in Markdown format as a table contains current prices.
1464        """
1465        infoText = ""
1466
1467        if show or self.pricesFile or onlyFiles:
1468            info = [
1469                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1470                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1471                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1472            ]
1473
1474            for item in iList:
1475                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1476                    item["ticker"],
1477                    item["figi"],
1478                    item["type"],
1479                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1480                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1481                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1482                    "{} / {}".format(
1483                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1484                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1485                    ),
1486                    "{} / {}".format(
1487                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1488                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1489                    ),
1490                    item["currency"],
1491                ))
1492
1493            infoText = "".join(info)
1494
1495            if show and not onlyFiles:
1496                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1497
1498            if self.pricesFile and (show or onlyFiles):
1499                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1500                    fH.write(infoText)
1501
1502                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1503
1504                if self.useHTMLReports:
1505                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1506                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1507                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1508
1509                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1510
1511        return infoText
1512
1513    def RequestTradingStatus(self) -> dict:
1514        """
1515        Requesting trading status for the instrument defined by `figi` variable.
1516
1517        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1518
1519        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1520
1521        :return: dictionary with trading status attributes. Response example:
1522                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1523                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1524        """
1525        if self._figi is None or not self._figi:
1526            uLogger.error("Variable `figi` must be defined for using this method!")
1527            raise Exception("FIGI required")
1528
1529        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1530
1531        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1532        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1533        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1534
1535        if self.moreDebug:
1536            uLogger.debug("Records about current trading status successfully received")
1537
1538        return tradingStatus
1539
1540    def RequestPortfolio(self) -> dict:
1541        """
1542        Requesting actual user's portfolio for current `accountId`.
1543
1544        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1545
1546        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1547
1548        :return: dictionary with user's portfolio.
1549        """
1550        if self.accountId is None or not self.accountId:
1551            uLogger.error("Variable `accountId` must be defined for using this method!")
1552            raise Exception("Account ID required")
1553
1554        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1555
1556        self.body = str({"accountId": self.accountId})
1557        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1558        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1559
1560        if self.moreDebug:
1561            uLogger.debug("Records about user's portfolio successfully received")
1562
1563        return rawPortfolio
1564
1565    def RequestPositions(self) -> dict:
1566        """
1567        Requesting open positions by currencies and instruments for current `accountId`.
1568
1569        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1570
1571        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1572
1573        :return: dictionary with open positions by instruments.
1574        """
1575        if self.accountId is None or not self.accountId:
1576            uLogger.error("Variable `accountId` must be defined for using this method!")
1577            raise Exception("Account ID required")
1578
1579        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1580
1581        self.body = str({"accountId": self.accountId})
1582        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1583        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1584
1585        if self.moreDebug:
1586            uLogger.debug("Records about current open positions successfully received")
1587
1588        return rawPositions
1589
1590    def RequestPendingOrders(self) -> list:
1591        """
1592        Requesting current actual pending limit orders for current `accountId`.
1593
1594        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1595
1596        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1597
1598        :return: list of dictionaries with pending limit orders.
1599        """
1600        if self.accountId is None or not self.accountId:
1601            uLogger.error("Variable `accountId` must be defined for using this method!")
1602            raise Exception("Account ID required")
1603
1604        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1605
1606        self.body = str({"accountId": self.accountId})
1607        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1608        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1609
1610        if "orders" in rawResponse.keys():
1611            rawOrders = rawResponse["orders"]
1612            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1613
1614        else:
1615            rawOrders = []
1616            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1617
1618        return rawOrders
1619
1620    def RequestStopOrders(self) -> list:
1621        """
1622        Requesting current actual stop orders for current `accountId`.
1623
1624        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1625
1626        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1627
1628        :return: list of dictionaries with stop orders.
1629        """
1630        if self.accountId is None or not self.accountId:
1631            uLogger.error("Variable `accountId` must be defined for using this method!")
1632            raise Exception("Account ID required")
1633
1634        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1635
1636        self.body = str({"accountId": self.accountId})
1637        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1638        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1639
1640        if "stopOrders" in rawResponse.keys():
1641            rawStopOrders = rawResponse["stopOrders"]
1642            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1643
1644        else:
1645            rawStopOrders = []
1646            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1647
1648        return rawStopOrders
1649
1650    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1651        """
1652        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1653        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1654        and `overviewBondsCalendarFile` are defined then also save information to file.
1655
1656        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1657        many requests about the state of the portfolio, and then, based on the received data, a large number
1658        of calculation and statistics are collected.
1659
1660        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1661        :param details: how detailed should the information be?
1662        - `full` — shows full available information about portfolio status (by default),
1663        - `positions` — shows only open positions,
1664        - `orders` — shows only sections of open limits and stop orders.
1665        - `digest` — show a short digest of the portfolio status,
1666        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1667        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1668        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1669        :return: dictionary with client's raw portfolio and some statistics.
1670        """
1671        if self.accountId is None or not self.accountId:
1672            uLogger.error("Variable `accountId` must be defined for using this method!")
1673            raise Exception("Account ID required")
1674
1675        view = {
1676            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1677                "headers": {},  # list of dictionaries, response headers without "positions" section
1678                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1679                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1680                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1681                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1682                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1683                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1684                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1685                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1686                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1687            },
1688            "stat": {  # --- some statistics calculated using "raw" sections:
1689                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1690                "availableRUB": 0.,  # available rubles (without other currencies)
1691                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1692                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1693                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1694                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1695                "sharesCostRUB": 0.,  # costs of all shares in RUB
1696                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1697                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1698                "futuresCostRUB": 0.,  # costs of all futures in RUB
1699                "Currencies": [],  # list of dictionaries of all currencies statistics
1700                "Shares": [],  # list of dictionaries of all shares statistics
1701                "Bonds": [],  # list of dictionaries of all bonds statistics
1702                "Etfs": [],  # list of dictionaries of all etfs statistics
1703                "Futures": [],  # list of dictionaries of all futures statistics
1704                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1705                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1706                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1707                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1708                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1709            },
1710            "analytics": {  # --- some analytics of portfolio:
1711                "distrByAssets": {},  # portfolio distribution by assets
1712                "distrByCompanies": {},  # portfolio distribution by companies
1713                "distrBySectors": {},  # portfolio distribution by sectors
1714                "distrByCurrencies": {},  # portfolio distribution by currencies
1715                "distrByCountries": {},  # portfolio distribution by countries
1716                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1717            }
1718        }
1719
1720        details = details.lower()
1721        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1722        if details not in availableDetails:
1723            details = "full"
1724            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1725
1726        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1727
1728        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1729        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1730        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1731        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1732
1733        # save response headers without "positions" section:
1734        for key in portfolioResponse.keys():
1735            if key != "positions":
1736                view["raw"]["headers"][key] = portfolioResponse[key]
1737
1738            else:
1739                continue
1740
1741        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1742        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1743        for item in portfolioResponse["positions"]:
1744            if item["instrumentType"] == "currency":
1745                self._figi = item["figi"]
1746                if not self._figi and item["ticker"]:
1747                    self._ticker = item["ticker"]
1748                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1749
1750                curr = self.SearchByFIGI(requestPrice=False)
1751
1752                # current price of currency in RUB:
1753                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1754                    "name": curr["name"],
1755                    "currentPrice": NanoToFloat(
1756                        item["currentPrice"]["units"],
1757                        item["currentPrice"]["nano"]
1758                    ),
1759                }
1760
1761                view["raw"]["Currencies"].append(item)
1762
1763            elif item["instrumentType"] == "share":
1764                view["raw"]["Shares"].append(item)
1765
1766            elif item["instrumentType"] == "bond":
1767                view["raw"]["Bonds"].append(item)
1768
1769            elif item["instrumentType"] == "etf":
1770                view["raw"]["Etfs"].append(item)
1771
1772            elif item["instrumentType"] == "futures":
1773                view["raw"]["Futures"].append(item)
1774
1775            else:
1776                continue
1777
1778        # how many volume of currencies (by ISO currency name) are blocked:
1779        for item in view["raw"]["positions"]["blocked"]:
1780            blocked = NanoToFloat(item["units"], item["nano"])
1781            if blocked > 0:
1782                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1783
1784        # how many volume of instruments (by FIGI) are blocked:
1785        for item in view["raw"]["positions"]["securities"]:
1786            blocked = int(item["blocked"])
1787            if blocked > 0:
1788                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1789
1790        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1791
1792        if "rub" in allBlocked.keys():
1793            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1794
1795        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1796        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1797        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1798        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1799        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1800        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1801        view["stat"]["portfolioCostRUB"] = sum([
1802            view["stat"]["allCurrenciesCostRUB"],
1803            view["stat"]["sharesCostRUB"],
1804            view["stat"]["bondsCostRUB"],
1805            view["stat"]["etfsCostRUB"],
1806            view["stat"]["futuresCostRUB"],
1807        ])
1808
1809        # --- calculating some portfolio statistics:
1810        byComp = {}  # distribution by companies
1811        bySect = {}  # distribution by sectors
1812        byCurr = {}  # distribution by currencies (include RUB)
1813        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1814        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1815
1816        for item in portfolioResponse["positions"]:
1817            self._figi = item["figi"]
1818            if not self._figi and item["ticker"]:
1819                self._ticker = item["ticker"]
1820                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1821
1822            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1823
1824            if instrument:
1825                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1826                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1827
1828                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1829                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1830
1831                else:
1832                    blocked = 0
1833
1834                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1835                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1836                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1837                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1838                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1839                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1840                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1841                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1842                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1843                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1844                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1845                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1846
1847                statData = {
1848                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1849                    "ticker": instrument["ticker"],  # ticker by FIGI
1850                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1851                    "volume": volume,  # available volume of instrument
1852                    "lots": lots,  # volume in lots of instrument
1853                    "direction": direction,  # direction of an instrument's position: short or long
1854                    "blocked": blocked,  # blocked volume of currency or instrument
1855                    "currentPrice": curPrice,  # current instrument's price in basic asset
1856                    "average": average,  # current average position price
1857                    "cost": cost,  # current cost of all volume of instrument in basic asset
1858                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1859                    "costRUB": costRUB,  # cost of instrument in ruble
1860                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1861                    "profit": profit,  # expected profit at current moment
1862                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1863                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1864                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1865                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1866                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1867                    "step": instrument["step"],  # minimum price increment
1868                }
1869
1870                # adding distribution by unique countries:
1871                if statData["country"] not in byCountry.keys():
1872                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1873
1874                else:
1875                    byCountry[statData["country"]]["cost"] += costRUB
1876                    byCountry[statData["country"]]["percent"] += percentCostRUB
1877
1878                if item["instrumentType"] != "currency":
1879                    # adding distribution by unique companies:
1880                    if statData["name"]:
1881                        if statData["name"] not in byComp.keys():
1882                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1883
1884                        else:
1885                            byComp[statData["name"]]["cost"] += costRUB
1886                            byComp[statData["name"]]["percent"] += percentCostRUB
1887
1888                    # adding distribution by unique sectors:
1889                    if statData["sector"] not in bySect.keys():
1890                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1891
1892                    else:
1893                        bySect[statData["sector"]]["cost"] += costRUB
1894                        bySect[statData["sector"]]["percent"] += percentCostRUB
1895
1896                # adding distribution by unique currencies:
1897                if currency not in byCurr.keys():
1898                    byCurr[currency] = {
1899                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1900                        "cost": costRUB,
1901                        "percent": percentCostRUB
1902                    }
1903
1904                else:
1905                    byCurr[currency]["cost"] += costRUB
1906                    byCurr[currency]["percent"] += percentCostRUB
1907
1908                # saving statistics for every instrument:
1909                if item["instrumentType"] == "currency":
1910                    view["stat"]["Currencies"].append(statData)
1911
1912                    # update dict with free funds for trading (total - blocked) by currencies
1913                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1914                    view["stat"]["funds"][currency] = {
1915                        "total": volume,
1916                        "totalCostRUB": costRUB,  # total volume cost in rubles
1917                        "free": volume - blocked,
1918                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1919                    }
1920
1921                elif item["instrumentType"] == "share":
1922                    view["stat"]["Shares"].append(statData)
1923
1924                elif item["instrumentType"] == "bond":
1925                    view["stat"]["Bonds"].append(statData)
1926
1927                elif item["instrumentType"] == "etf":
1928                    view["stat"]["Etfs"].append(statData)
1929
1930                elif item["instrumentType"] == "Futures":
1931                    view["stat"]["Futures"].append(statData)
1932
1933                else:
1934                    continue
1935
1936        # total changes in Russian Ruble:
1937        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1938        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1939        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1940        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1941        view["stat"]["funds"]["rub"] = {
1942            "total": view["stat"]["availableRUB"],
1943            "totalCostRUB": view["stat"]["availableRUB"],
1944            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1945            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1946        }
1947
1948        # --- pending limit orders sector data:
1949        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1950        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1951
1952        for item in view["raw"]["orders"]:
1953            self._figi = item["figi"]
1954
1955            if item["figi"] not in uniquePendingOrdersFIGIs:
1956                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1957
1958                uniquePendingOrdersFIGIs.append(item["figi"])
1959                uniquePendingOrders[item["figi"]] = instrument
1960
1961            else:
1962                instrument = uniquePendingOrders[item["figi"]]
1963
1964            if instrument:
1965                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1966                orderType = TKS_ORDER_TYPES[item["orderType"]]
1967                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1968                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1969
1970                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1971                if item["direction"] == "ORDER_DIRECTION_BUY":
1972                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1973
1974                else:
1975                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1976
1977                # requested price for order execution:
1978                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1979
1980                # necessary changes in percent to reach target from current price:
1981                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1982
1983                view["stat"]["orders"].append({
1984                    "orderID": item["orderId"],  # orderId number parameter of current order
1985                    "figi": item["figi"],  # FIGI identification
1986                    "ticker": instrument["ticker"],  # ticker name by FIGI
1987                    "lotsRequested": item["lotsRequested"],  # requested lots value
1988                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1989                    "currentPrice": lastPrice,  # current instrument's price for defined action
1990                    "targetPrice": target,  # requested price for order execution in base currency
1991                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1992                    "percentChanges": changes,  # changes in percent to target from current price
1993                    "currency": item["currency"],  # instrument's currency name
1994                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1995                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1996                    "status": orderState,  # order status from TKS_ORDER_STATES
1997                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1998                })
1999
2000        # --- stop orders sector data:
2001        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2002        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2003
2004        for item in view["raw"]["stopOrders"]:
2005            self._figi = item["figi"]
2006
2007            if item["figi"] not in uniqueStopOrdersFIGIs:
2008                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2009
2010                uniqueStopOrdersFIGIs.append(item["figi"])
2011                uniqueStopOrders[item["figi"]] = instrument
2012
2013            else:
2014                instrument = uniqueStopOrders[item["figi"]]
2015
2016            if instrument:
2017                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2018                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2019                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2020
2021                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2022                if "expirationTime" in item.keys():
2023                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2024                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2025
2026                else:
2027                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2028                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2029
2030                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2031                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2032                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2033
2034                else:
2035                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2036
2037                # requested price when stop-order executed:
2038                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2039
2040                # price for limit-order, set up when stop-order executed:
2041                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2042
2043                # necessary changes in percent to reach target from current price:
2044                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2045
2046                view["stat"]["stopOrders"].append({
2047                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2048                    "figi": item["figi"],  # FIGI identification
2049                    "ticker": instrument["ticker"],  # ticker name by FIGI
2050                    "lotsRequested": item["lotsRequested"],  # requested lots value
2051                    "currentPrice": lastPrice,  # current instrument's price for defined action
2052                    "targetPrice": target,  # requested price for stop-order execution in base currency
2053                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2054                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2055                    "percentChanges": changes,  # changes in percent to target from current price
2056                    "currency": item["currency"],  # instrument's currency name
2057                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2058                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2059                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2060                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2061                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2062                })
2063
2064        # --- calculating data for analytics section:
2065        # portfolio distribution by assets:
2066        view["analytics"]["distrByAssets"] = {
2067            "Ruble": {
2068                "uniques": 1,
2069                "cost": view["stat"]["availableRUB"],
2070                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2071            },
2072            "Currencies": {
2073                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2074                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2075                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2076            },
2077            "Shares": {
2078                "uniques": len(view["stat"]["Shares"]),
2079                "cost": view["stat"]["sharesCostRUB"],
2080                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2081            },
2082            "Bonds": {
2083                "uniques": len(view["stat"]["Bonds"]),
2084                "cost": view["stat"]["bondsCostRUB"],
2085                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2086            },
2087            "Etfs": {
2088                "uniques": len(view["stat"]["Etfs"]),
2089                "cost": view["stat"]["etfsCostRUB"],
2090                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2091            },
2092            "Futures": {
2093                "uniques": len(view["stat"]["Futures"]),
2094                "cost": view["stat"]["futuresCostRUB"],
2095                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2096            },
2097        }
2098
2099        # portfolio distribution by companies:
2100        view["analytics"]["distrByCompanies"]["All money cash"] = {
2101            "ticker": "",
2102            "cost": view["stat"]["allCurrenciesCostRUB"],
2103            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2104        }
2105        view["analytics"]["distrByCompanies"].update(byComp)
2106
2107        # portfolio distribution by sectors:
2108        view["analytics"]["distrBySectors"]["All money cash"] = {
2109            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2110            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2111        }
2112        view["analytics"]["distrBySectors"].update(bySect)
2113
2114        # portfolio distribution by currencies:
2115        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2116            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2117
2118            if self.moreDebug:
2119                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2120
2121        view["analytics"]["distrByCurrencies"].update(byCurr)
2122        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2123        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2124
2125        # portfolio distribution by countries:
2126        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2127            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2128
2129            if self.moreDebug:
2130                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2131
2132        view["analytics"]["distrByCountries"].update(byCountry)
2133        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2134        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2135
2136        # --- Prepare text statistics overview in human-readable:
2137        if show or onlyFiles:
2138            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2139
2140            # Whatever the value `details`, header not changes:
2141            info = [
2142                "# Client's portfolio\n\n",
2143                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2144                "* **Account ID:** [{}]\n".format(self.accountId),
2145            ]
2146
2147            if details in ["full", "positions", "digest"]:
2148                info.extend([
2149                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2150                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2151                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2152                        view["stat"]["totalChangesRUB"],
2153                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2154                        view["stat"]["totalChangesPercentRUB"],
2155                    ),
2156                ])
2157
2158            if details in ["full", "positions"]:
2159                info.extend([
2160                    "## Open positions\n\n",
2161                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2162                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2163                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2164                        "{:.2f} ({:.2f}) rub".format(
2165                            view["stat"]["availableRUB"],
2166                            view["stat"]["blockedRUB"],
2167                        )
2168                    )
2169                ])
2170
2171                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2172                    return [
2173                        "|                             |                                 |          |              |              |                     |                              |\n",
2174                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2175                            noTradeStr if noTradeStr else typeStr,
2176                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2177                        ),
2178                    ]
2179
2180                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2181                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2182                        "{} [{}]".format(data["ticker"], data["figi"]),
2183                        "{:.2f} ({:.2f}) {}".format(
2184                            data["volume"],
2185                            data["blocked"],
2186                            data["currency"],
2187                        ) if isCurr else "{:.0f} ({:.0f})".format(
2188                            data["volume"],
2189                            data["blocked"],
2190                        ),
2191                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2192                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2193                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2194                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2195                        "{}{:.2f} {} ({}{:.2f}%)".format(
2196                            "+" if data["profit"] > 0 else "",
2197                            data["profit"], data["baseCurrencyName"],
2198                            "+" if data["percentProfit"] > 0 else "",
2199                            data["percentProfit"],
2200                        ),
2201                    )
2202
2203                # --- Show currencies section:
2204                if view["stat"]["Currencies"]:
2205                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2206                    for item in view["stat"]["Currencies"]:
2207                        info.append(_InfoStr(item, isCurr=True))
2208
2209                else:
2210                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2211
2212                # --- Show shares section:
2213                if view["stat"]["Shares"]:
2214                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2215
2216                    for item in view["stat"]["Shares"]:
2217                        info.append(_InfoStr(item))
2218
2219                else:
2220                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2221
2222                # --- Show bonds section:
2223                if view["stat"]["Bonds"]:
2224                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2225
2226                    for item in view["stat"]["Bonds"]:
2227                        info.append(_InfoStr(item))
2228
2229                else:
2230                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2231
2232                # --- Show etfs section:
2233                if view["stat"]["Etfs"]:
2234                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2235
2236                    for item in view["stat"]["Etfs"]:
2237                        info.append(_InfoStr(item))
2238
2239                else:
2240                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2241
2242                # --- Show futures section:
2243                if view["stat"]["Futures"]:
2244                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2245
2246                    for item in view["stat"]["Futures"]:
2247                        info.append(_InfoStr(item))
2248
2249                else:
2250                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2251
2252            if details in ["full", "orders"]:
2253                # --- Show pending limit orders section:
2254                if view["stat"]["orders"]:
2255                    info.extend([
2256                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2257                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2258                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2259                    ])
2260
2261                    for item in view["stat"]["orders"]:
2262                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2263                            "{} [{}]".format(item["ticker"], item["figi"]),
2264                            item["orderID"],
2265                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2266                            "{} {} ({}{:.2f}%)".format(
2267                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2268                                item["baseCurrencyName"],
2269                                "+" if item["percentChanges"] > 0 else "",
2270                                float(item["percentChanges"]),
2271                            ),
2272                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2273                            item["action"],
2274                            item["type"],
2275                            item["date"],
2276                        ))
2277
2278                else:
2279                    info.append("\n## Total pending limit-orders: [0]\n")
2280
2281                # --- Show stop orders section:
2282                if view["stat"]["stopOrders"]:
2283                    info.extend([
2284                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2285                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2286                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2287                    ])
2288
2289                    for item in view["stat"]["stopOrders"]:
2290                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2291                            "{} [{}]".format(item["ticker"], item["figi"]),
2292                            item["orderID"],
2293                            item["lotsRequested"],
2294                            "{} {} ({}{:.2f}%)".format(
2295                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2296                                item["baseCurrencyName"],
2297                                "+" if item["percentChanges"] > 0 else "",
2298                                float(item["percentChanges"]),
2299                            ),
2300                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2301                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2302                            item["action"],
2303                            item["type"],
2304                            item["expType"],
2305                            item["createDate"],
2306                            item["expDate"],
2307                        ))
2308
2309                else:
2310                    info.append("\n## Total stop-orders: [0]\n")
2311
2312            if details in ["full", "analytics"]:
2313                # -- Show analytics section:
2314                if view["stat"]["portfolioCostRUB"] > 0:
2315                    info.extend([
2316                        "\n# Analytics\n\n"
2317                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2318                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2319                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2320                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2321                            view["stat"]["totalChangesRUB"],
2322                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2323                            view["stat"]["totalChangesPercentRUB"],
2324                        ),
2325                        "\n## Portfolio distribution by assets\n"
2326                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2327                        "|------------------------------------|---------|---------|--------------------|\n",
2328                    ])
2329
2330                    for key in view["analytics"]["distrByAssets"].keys():
2331                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2332                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2333                                key,
2334                                view["analytics"]["distrByAssets"][key]["uniques"],
2335                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2336                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2337                            ))
2338
2339                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2340
2341                    info.extend([
2342                        "\n## Portfolio distribution by companies\n"
2343                        "\n| Company                                      | Percent | Current cost       |\n",
2344                        aSepLine,
2345                    ])
2346
2347                    for company in view["analytics"]["distrByCompanies"].keys():
2348                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2349                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2350                                "{}{}".format(
2351                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2352                                    company,
2353                                ),
2354                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2355                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2356                            ))
2357
2358                    info.extend([
2359                        "\n## Portfolio distribution by sectors\n"
2360                        "\n| Sector                                       | Percent | Current cost       |\n",
2361                        aSepLine,
2362                    ])
2363
2364                    for sector in view["analytics"]["distrBySectors"].keys():
2365                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2366                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2367                                sector,
2368                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2369                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2370                            ))
2371
2372                    info.extend([
2373                        "\n## Portfolio distribution by currencies\n"
2374                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2375                        aSepLine,
2376                    ])
2377
2378                    for curr in view["analytics"]["distrByCurrencies"].keys():
2379                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2380                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2381                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2382                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2383                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2384                            ))
2385
2386                    info.extend([
2387                        "\n## Portfolio distribution by countries\n"
2388                        "\n| Assets by country                            | Percent | Current cost       |\n",
2389                        aSepLine,
2390                    ])
2391
2392                    for country in view["analytics"]["distrByCountries"].keys():
2393                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2394                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2395                                country,
2396                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2397                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2398                            ))
2399
2400            if details in ["full", "calendar"]:
2401                # -- Show bonds payment calendar section:
2402                if view["stat"]["Bonds"]:
2403                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2404                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2405                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2406
2407                else:
2408                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2409
2410            infoText = "".join(info)
2411
2412            if show and not onlyFiles:
2413                uLogger.info(infoText)
2414
2415            if details == "full" and self.overviewFile:
2416                filename = self.overviewFile
2417
2418            elif details == "digest" and self.overviewDigestFile:
2419                filename = self.overviewDigestFile
2420
2421            elif details == "positions" and self.overviewPositionsFile:
2422                filename = self.overviewPositionsFile
2423
2424            elif details == "orders" and self.overviewOrdersFile:
2425                filename = self.overviewOrdersFile
2426
2427            elif details == "analytics" and self.overviewAnalyticsFile:
2428                filename = self.overviewAnalyticsFile
2429
2430            elif details == "calendar" and self.overviewBondsCalendarFile:
2431                filename = self.overviewBondsCalendarFile
2432
2433            else:
2434                filename = ""
2435
2436            if filename and (show or onlyFiles):
2437                with open(filename, "w", encoding="UTF-8") as fH:
2438                    fH.write(infoText)
2439
2440                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2441
2442                if self.useHTMLReports:
2443                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2444                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2445                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2446
2447                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2448
2449        return view
2450
2451    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2452        """
2453        Returns history operations between two given dates for current `accountId`.
2454        If `reportFile` string is not empty then also save human-readable report.
2455        Shows some statistical data of closed positions.
2456
2457        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2458        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2459        :param show: if `True` then also prints all records to the console.
2460        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2461        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2462        :return: original list of dictionaries with history of deals records from API ("operations" key):
2463                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2464                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2465        """
2466        if self.accountId is None or not self.accountId:
2467            uLogger.error("Variable `accountId` must be defined for using this method!")
2468            raise Exception("Account ID required")
2469
2470        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2471
2472        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2473
2474        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2475        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2476        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2477        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2478        customStat = {}  # custom statistics in additional to responseJSON
2479
2480        # --- output report in human-readable format:
2481        if self.reportFile and (show or onlyFiles):
2482            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2483            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2484            nextDay = ""
2485
2486            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2487
2488            if len(ops) > 0:
2489                customStat = {
2490                    "opsCount": 0,  # total operations count
2491                    "buyCount": 0,  # buy operations
2492                    "sellCount": 0,  # sell operations
2493                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2494                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2495                    "payIn": {"rub": 0.},  # Deposit brokerage account
2496                    "payOut": {"rub": 0.},  # Withdrawals
2497                    "divs": {"rub": 0.},  # Dividends income
2498                    "coupons": {"rub": 0.},  # Coupon's income
2499                    "brokerCom": {"rub": 0.},  # Service commissions
2500                    "serviceCom": {"rub": 0.},  # Service commissions
2501                    "marginCom": {"rub": 0.},  # Margin commissions
2502                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2503                }
2504
2505                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2506                for item in ops:
2507                    if item["state"] == "OPERATION_STATE_EXECUTED":
2508                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2509
2510                        # count buy operations:
2511                        if "_BUY" in item["operationType"]:
2512                            customStat["buyCount"] += 1
2513
2514                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2515                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2516
2517                            else:
2518                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2519
2520                        # count sell operations:
2521                        elif "_SELL" in item["operationType"]:
2522                            customStat["sellCount"] += 1
2523
2524                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2525                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2529
2530                        # count incoming operations:
2531                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2532                            if item["payment"]["currency"] in customStat["payIn"].keys():
2533                                customStat["payIn"][item["payment"]["currency"]] += payment
2534
2535                            else:
2536                                customStat["payIn"][item["payment"]["currency"]] = payment
2537
2538                        # count withdrawals operations:
2539                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2540                            if item["payment"]["currency"] in customStat["payOut"].keys():
2541                                customStat["payOut"][item["payment"]["currency"]] += payment
2542
2543                            else:
2544                                customStat["payOut"][item["payment"]["currency"]] = payment
2545
2546                        # count dividends income:
2547                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2548                            if item["payment"]["currency"] in customStat["divs"].keys():
2549                                customStat["divs"][item["payment"]["currency"]] += payment
2550
2551                            else:
2552                                customStat["divs"][item["payment"]["currency"]] = payment
2553
2554                        # count coupon's income:
2555                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2556                            if item["payment"]["currency"] in customStat["coupons"].keys():
2557                                customStat["coupons"][item["payment"]["currency"]] += payment
2558
2559                            else:
2560                                customStat["coupons"][item["payment"]["currency"]] = payment
2561
2562                        # count broker commissions:
2563                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2564                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2565                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2566
2567                            else:
2568                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2569
2570                        # count service commissions:
2571                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2572                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2573                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2574
2575                            else:
2576                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2577
2578                        # count margin commissions:
2579                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2580                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2581                                customStat["marginCom"][item["payment"]["currency"]] += payment
2582
2583                            else:
2584                                customStat["marginCom"][item["payment"]["currency"]] = payment
2585
2586                        # count withholding taxes:
2587                        elif "_TAX" in item["operationType"]:
2588                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2589                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2590
2591                            else:
2592                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2593
2594                        else:
2595                            continue
2596
2597                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2598
2599                # --- view "Actions" lines:
2600                info.extend([
2601                    "| Report sections            |                               |                              |                      |                        |\n",
2602                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2603                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2604                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2605                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2606                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2607                    ),
2608                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2609                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2610                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2611                    ),
2612                ])
2613
2614                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2615                for key in opsKeys:
2616                    if key == "rub":
2617                        continue
2618
2619                    info.extend([
2620                        "|                            |                               | {:<28} |                      |                        |\n".format(
2621                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2622                        ),
2623                        "|                            |                               | {:<28} |                      |                        |\n".format(
2624                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2625                        ),
2626                    ])
2627
2628                info.append(splitLine1)
2629
2630                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2631                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2632                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2633                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2634                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2635                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2636                    )
2637
2638                # --- view "Payments" lines:
2639                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2640                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2641
2642                for key in paymentsKeys:
2643                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2644
2645                info.append(splitLine1)
2646
2647                # --- view "Commissions and taxes" lines:
2648                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2649                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2650
2651                for key in comKeys:
2652                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2653
2654                info.extend([
2655                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2656                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2657                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2658                ])
2659
2660            else:
2661                info.append("Broker returned no operations during this period\n")
2662
2663            # --- view "Operations" section:
2664            for item in ops:
2665                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2666                    continue
2667
2668                else:
2669                    self._figi = item["figi"]
2670                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2671                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2672
2673                    # group of deals during one day:
2674                    if nextDay and item["date"].split("T")[0] != nextDay:
2675                        info.append(splitLine2)
2676                        nextDay = ""
2677
2678                    else:
2679                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2680
2681                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2682                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2683                        self._figi if self._figi else "—",
2684                        instrument["ticker"] if instrument else "—",
2685                        instrument["type"] if instrument else "—",
2686                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2687                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2688                        TKS_OPERATION_STATES[item["state"]],
2689                        TKS_OPERATION_TYPES[item["operationType"]],
2690                    ))
2691
2692            infoText = "".join(info)
2693
2694            if show and not onlyFiles:
2695                if self.moreDebug:
2696                    uLogger.debug("Records about history of a client's operations successfully received")
2697
2698                uLogger.info(infoText)
2699
2700            if self.reportFile and (show or onlyFiles):
2701                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2702                    fH.write(infoText)
2703
2704                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2705
2706                if self.useHTMLReports:
2707                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2708                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2709                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2710
2711                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2712
2713        return ops, customStat
2714
2715    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2716        """
2717        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2718
2719        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2720        Warning! Broker server used ISO UTC time by default.
2721
2722        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2723        Also, `historyFile` used to update history with `onlyMissing` parameter.
2724
2725        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2726
2727        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2728        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2729        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2730                         `"hour"`, `"day"`. Default: `"hour"`.
2731        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2732                            False by default. Warning! History appends only from last candle to current time
2733                            with always update last candle!
2734        :param csvSep: separator if csv-file is used, `,` by default.
2735        :param show: if `True` then also prints Pandas DataFrame to the console.
2736        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2737        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2738                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2739        """
2740        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2741        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2742        history = None  # empty pandas object for history
2743
2744        if interval not in TKS_CANDLE_INTERVALS.keys():
2745            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2746            raise Exception("Incorrect value")
2747
2748        if not (self._ticker or self._figi):
2749            uLogger.error("Ticker or FIGI must be defined!")
2750            raise Exception("Ticker or FIGI required")
2751
2752        if self._ticker and not self._figi:
2753            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2754            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2755
2756        if self._figi and not self._ticker:
2757            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2758            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2759
2760        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2761        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2762        if interval.lower() != "day":
2763            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2764
2765        delta = dtEnd - dtStart  # current UTC time minus last time in file
2766        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2767
2768        # calculate history length in candles:
2769        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2770        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2771            length += 1  # to avoid fraction time
2772
2773        # calculate data blocks count:
2774        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2775
2776        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2777        if self.moreDebug:
2778            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2779            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2780            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2781            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2782
2783        tempOld = None  # pandas object for old history, if --only-missing key present
2784        lastTime = None  # datetime object of last old candle in file
2785
2786        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2787            if self.moreDebug:
2788                uLogger.debug("--only-missing key present, add only last missing candles...")
2789                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2790
2791            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2792
2793            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2794            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2795            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2796            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2797
2798            # get last datetime object from last string in file or minus 1 delta if file is empty:
2799            if len(tempOld) > 0:
2800                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2801
2802            else:
2803                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2804
2805            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2806
2807        responseJSONs = []  # raw history blocks of data
2808
2809        blockEnd = dtEnd
2810        for item in range(blocks):
2811            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2812            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2813
2814            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2815                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2816            ))
2817
2818            if blockStart == blockEnd:
2819                uLogger.debug("Skipped this zero-length block...")
2820
2821            else:
2822                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2823                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2824                self.body = str({
2825                    "figi": self._figi,
2826                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2827                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2828                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2829                })
2830                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2831
2832                if "code" in responseJSON.keys():
2833                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2834
2835                else:
2836                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2837                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2838
2839                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2840
2841            blockEnd = blockStart
2842
2843        printCount = len(responseJSONs)  # candles to show in console
2844        if responseJSONs:
2845            tempHistory = pd.DataFrame(
2846                data={
2847                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2848                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2849                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2850                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2851                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2852                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2853                    "volume": [int(item["volume"]) for item in responseJSONs],
2854                },
2855                index=range(len(responseJSONs)),
2856                columns=["date", "time", "open", "high", "low", "close", "volume"],
2857            )
2858            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2859            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2860
2861            # append only newest candles to old history if --only-missing key present:
2862            if onlyMissing and tempOld is not None and lastTime is not None:
2863                index = 0  # find start index in tempHistory data:
2864
2865                for i, item in tempHistory.iterrows():
2866                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2867
2868                    if curTime == lastTime:
2869                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2870                        index = i
2871                        printCount = i + 1
2872                        break
2873
2874                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2875
2876            else:
2877                history = tempHistory  # if no `--only-missing` key then load full data from server
2878
2879            if self.moreDebug:
2880                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2881
2882        if history is not None and not history.empty:
2883            if show and not onlyFiles:
2884                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2885                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2886                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2887                ))
2888
2889        else:
2890            uLogger.warning("Received an empty candles history!")
2891
2892        if self.historyFile is not None:
2893            if history is not None and not history.empty:
2894                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2895                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2896
2897            else:
2898                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2899
2900        else:
2901            if self.moreDebug:
2902                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2903
2904        return history
2905
2906    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2907        """
2908        Load candles history from csv-file and return Pandas DataFrame object.
2909
2910        See also: `History()` and `ShowHistoryChart()` methods.
2911
2912        :param filePath: path to csv-file to open.
2913        """
2914        loadedHistory = None  # init candles data object
2915
2916        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2917
2918        if os.path.exists(filePath):
2919            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2920
2921            tfStr = self.priceModel.FormattedDelta(
2922                self.priceModel.timeframe,
2923                "{days} days {hours}h {minutes}m {seconds}s",
2924            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2925                self.priceModel.timeframe,
2926                "{hours}h {minutes}m {seconds}s",
2927            )
2928
2929            if loadedHistory is not None and not loadedHistory.empty:
2930                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2931                    len(loadedHistory),
2932                    tfStr,
2933                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2934                )
2935
2936            else:
2937                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2938
2939        else:
2940            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2941
2942        return loadedHistory
2943
2944    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2945        """
2946        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2947
2948        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2949        Default: `index.html` (both for interact and non-interact candlesticks chart).
2950
2951        See also: `History()` and `LoadHistory()` methods.
2952
2953        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2954        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2955                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2956                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2957                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2958        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2959                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2960        """
2961        if isinstance(candles, str):
2962            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2963            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2964
2965        elif isinstance(candles, pd.DataFrame):
2966            self.priceModel.prices = candles  # set candles chain from variable
2967            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2968
2969            if "datetime" not in candles.columns:
2970                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2971
2972        else:
2973            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2974            raise Exception("Incorrect value")
2975
2976        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2977
2978        if interact:
2979            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2980
2981            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2982
2983        else:
2984            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2985
2986            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2987
2988        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2989
2990    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2991        """
2992        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2993        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2994
2995        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2996
2997        :param operation: string "Buy" or "Sell".
2998        :param lots: volume, integer count of lots >= 1.
2999        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
3000        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3001        :param expDate: string "Undefined" by default or local date in future,
3002                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3003        :return: JSON with response from broker server.
3004        """
3005        if self.accountId is None or not self.accountId:
3006            uLogger.error("Variable `accountId` must be defined for using this method!")
3007            raise Exception("Account ID required")
3008
3009        if operation is None or not operation or operation not in ("Buy", "Sell"):
3010            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3011            raise Exception("Incorrect value")
3012
3013        if lots is None or lots < 1:
3014            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3015            lots = 1
3016
3017        if tp is None or tp < 0:
3018            tp = 0
3019
3020        if sl is None or sl < 0:
3021            sl = 0
3022
3023        if expDate is None or not expDate:
3024            expDate = "Undefined"
3025
3026        if not (self._ticker or self._figi):
3027            uLogger.error("Ticker or FIGI must be defined!")
3028            raise Exception("Ticker or FIGI required")
3029
3030        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3031        self._ticker = instrument["ticker"]
3032        self._figi = instrument["figi"]
3033
3034        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3035
3036        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3037        self.body = str({
3038            "figi": self._figi,
3039            "quantity": str(lots),
3040            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3041            "accountId": str(self.accountId),
3042            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3043        })
3044        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3045
3046        if "orderId" in response.keys():
3047            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3048                operation, response["orderId"],
3049                self._ticker, self._figi, lots,
3050                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3051                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3052                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3053            ))
3054
3055            if tp > 0:
3056                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3057
3058            if sl > 0:
3059                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3060
3061        else:
3062            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3063
3064        return response
3065
3066    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3067        """
3068        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3069        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3070
3071        See also: `Order()` and `Trade()` docstrings.
3072
3073        :param lots: volume, integer count of lots >= 1.
3074        :param tp: float > 0, take profit price of stop-order.
3075        :param sl: float > 0, stop loss price of stop-order.
3076        :param expDate: it's a local date in future.
3077                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3078        :return: JSON with response from broker server.
3079        """
3080        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3081
3082    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3083        """
3084        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3085        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3086
3087        See also: `Order()` and `Trade()` docstrings.
3088
3089        :param lots: volume, integer count of lots >= 1.
3090        :param tp: float > 0, take profit price of stop-order.
3091        :param sl: float > 0, stop loss price of stop-order.
3092        :param expDate: it's a local date in the future.
3093                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3094        :return: JSON with response from broker server.
3095        """
3096        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3097
3098    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3099        """
3100        Close position of given instruments.
3101
3102        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3103        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3104                         This avoids unnecessary downloading data from the server.
3105        """
3106        if instruments is None or not instruments:
3107            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3108            raise Exception("Ticker or FIGI required")
3109
3110        if isinstance(instruments, str):
3111            instruments = [instruments]
3112
3113        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3114        if uniqueInstruments:
3115            if portfolio is None or not portfolio:
3116                portfolio = self.Overview(show=False)
3117
3118            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3119            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3120
3121            for self._figi in uniqueInstruments:
3122                if self._figi not in allOpened:
3123                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3124                    continue
3125
3126                # search open trade info about instrument by ticker:
3127                instrument = {}
3128                for iType in TKS_INSTRUMENTS:
3129                    if instrument:
3130                        break
3131
3132                    for item in portfolio["stat"][iType]:
3133                        if item["figi"] == self._figi:
3134                            instrument = item
3135                            break
3136
3137                if instrument:
3138                    self._ticker = instrument["ticker"]
3139                    self._figi = instrument["figi"]
3140
3141                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3142                        self._ticker,
3143                        self._figi,
3144                        int(instrument["volume"]),
3145                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3146                    ))
3147
3148                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3149
3150                    if tradeLots > 0:
3151                        if instrument["blocked"] > 0:
3152                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3153                                instrument["blocked"],
3154                                self._ticker,
3155                                tradeLots,
3156                            ))
3157
3158                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3159                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3160
3161                    else:
3162                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3163
3164    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3165        """
3166        Close all positions of given instruments with defined type.
3167
3168        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3169        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3170                         This avoids unnecessary downloading data from the server.
3171        """
3172        if iType not in TKS_INSTRUMENTS:
3173            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3174
3175        else:
3176            if portfolio is None or not portfolio:
3177                portfolio = self.Overview(show=False)
3178
3179            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3180            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3181
3182            if tickers and portfolio:
3183                self.CloseTrades(tickers, portfolio)
3184
3185            else:
3186                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3187
3188    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3189        """
3190        Universal method to create market or limit orders with all available parameters for current `accountId`.
3191        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3192
3193        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3194        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3195
3196        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3197        then broker immediately open market order as you can do simple --buy or --sell operations!
3198
3199        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3200        When current price will go up or down to target price value then broker opens a limit order.
3201        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3202
3203        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3204
3205        :param operation: string "Buy" or "Sell".
3206        :param orderType: string "Limit" or "Stop".
3207        :param lots: volume, integer count of lots >= 1.
3208        :param targetPrice: target price > 0. This is open trade price for limit order.
3209        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3210                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3211        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3212                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3213                         Stop loss order always executed by market price.
3214        :param expDate: string "Undefined" by default or local date in future.
3215                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3216                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3217                        A limit order has no expiration date, it lasts until the end of the trading day.
3218        :return: JSON with response from broker server.
3219        """
3220        if self.accountId is None or not self.accountId:
3221            uLogger.error("Variable `accountId` must be defined for using this method!")
3222            raise Exception("Account ID required")
3223
3224        if operation is None or not operation or operation not in ("Buy", "Sell"):
3225            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3226            raise Exception("Incorrect value")
3227
3228        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3229            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3230            raise Exception("Incorrect value")
3231
3232        if lots is None or lots < 1:
3233            uLogger.error("You must define trade volume > 0: integer count of lots!")
3234            raise Exception("Incorrect value")
3235
3236        if targetPrice is None or targetPrice <= 0:
3237            uLogger.error("Target price for limit-order must be greater than 0!")
3238            raise Exception("Incorrect value")
3239
3240        if limitPrice is None or limitPrice <= 0:
3241            limitPrice = targetPrice
3242
3243        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3244            stopType = "Limit"
3245
3246        if expDate is None or not expDate:
3247            expDate = "Undefined"
3248
3249        if not (self._ticker or self._figi):
3250            uLogger.error("Tocker or FIGI must be defined!")
3251            raise Exception("Ticker or FIGI required")
3252
3253        response = {}
3254        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3255        self._ticker = instrument["ticker"]
3256        self._figi = instrument["figi"]
3257
3258        if orderType == "Limit":
3259            uLogger.debug(
3260                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3261                    self._ticker, self._figi,
3262                    operation, lots, targetPrice, instrument["currency"],
3263                ))
3264
3265            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3266            self.body = str({
3267                "figi": self._figi,
3268                "quantity": str(lots),
3269                "price": FloatToNano(targetPrice),
3270                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3271                "accountId": str(self.accountId),
3272                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3273            })
3274            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3275
3276            if "orderId" in response.keys():
3277                uLogger.info(
3278                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3279                        response["orderId"], self._ticker, self._figi, operation, lots,
3280                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3281                    ))
3282
3283                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3284                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3285                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3286                            targetPrice, instrument["currency"],
3287                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3288                        ))
3289
3290                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3291                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3292                            targetPrice, instrument["currency"],
3293                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3294                        ))
3295
3296            else:
3297                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3298
3299        if orderType == "Stop":
3300            uLogger.debug(
3301                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3302                    self._ticker, self._figi,
3303                    operation, lots,
3304                    targetPrice, instrument["currency"],
3305                    limitPrice, instrument["currency"],
3306                    stopType, expDate,
3307                ))
3308
3309            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3310            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3311            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3312
3313            body = {
3314                "figi": self._figi,
3315                "quantity": str(lots),
3316                "price": FloatToNano(limitPrice),
3317                "stopPrice": FloatToNano(targetPrice),
3318                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3319                "accountId": str(self.accountId),
3320                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3321                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3322            }
3323
3324            if expDateUTC:
3325                body["expireDate"] = expDateUTC
3326
3327            self.body = str(body)
3328            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3329
3330            if "stopOrderId" in response.keys():
3331                uLogger.info(
3332                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3333                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3334                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3335                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3336                        TKS_STOP_ORDER_TYPES[stopOrderType],
3337                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3338                    ))
3339
3340                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3341                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3342                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3343                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3344                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3345                        ))
3346
3347                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3348                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3349                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3350                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3351                        ))
3352
3353            else:
3354                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3355
3356        return response
3357
3358    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3359        """
3360        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3361        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3362        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3363        See also: `Order()` docstring.
3364
3365        :param lots: volume, integer count of lots >= 1.
3366        :param targetPrice: target price > 0. This is open trade price for limit order.
3367        :return: JSON with response from broker server.
3368        """
3369        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3370
3371    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3372        """
3373        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3374        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3375        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3376        target price value then broker opens a limit order. See also: `Order()` docstring.
3377
3378        :param lots: volume, integer count of lots >= 1.
3379        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3380        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3381                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3382        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3383                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3384        :param expDate: string "Undefined" by default or local date in future.
3385                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3386                        This date is converting to UTC format for server.
3387        :return: JSON with response from broker server.
3388        """
3389        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3390
3391    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3392        """
3393        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3394        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3395        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3396        See also: `Order()` docstring.
3397
3398        :param lots: volume, integer count of lots >= 1.
3399        :param targetPrice: target price > 0. This is open trade price for limit order.
3400        :return: JSON with response from broker server.
3401        """
3402        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3403
3404    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3405        """
3406        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3407        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3408        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3409        target price value then broker opens a limit order. See also: `Order()` docstring.
3410
3411        :param lots: volume, integer count of lots >= 1.
3412        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3413        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3414                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3415        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3416                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3417        :param expDate: string "Undefined" by default or local date in future.
3418                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3419                        This date is converting to UTC format for server.
3420        :return: JSON with response from broker server.
3421        """
3422        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3423
3424    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3425        """
3426        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3427
3428        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3429        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3430                             This avoids unnecessary downloading data from the server.
3431        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3432        """
3433        if self.accountId is None or not self.accountId:
3434            uLogger.error("Variable `accountId` must be defined for using this method!")
3435            raise Exception("Account ID required")
3436
3437        if orderIDs:
3438            if allOrdersIDs is None:
3439                rawOrders = self.RequestPendingOrders()
3440                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3441
3442            if allStopOrdersIDs is None:
3443                rawStopOrders = self.RequestStopOrders()
3444                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3445
3446            for orderID in orderIDs:
3447                idInPendingOrders = orderID in allOrdersIDs
3448                idInStopOrders = orderID in allStopOrdersIDs
3449
3450                if not (idInPendingOrders or idInStopOrders):
3451                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3452                    continue
3453
3454                else:
3455                    if idInPendingOrders:
3456                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3457
3458                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3459                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3460                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3461                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3462
3463                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3464                            if self.moreDebug:
3465                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3466
3467                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3468
3469                        else:
3470                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3471
3472                    elif idInStopOrders:
3473                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3474
3475                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3476                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3477                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3478                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3479
3480                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3481                            if self.moreDebug:
3482                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3483
3484                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3485
3486                        else:
3487                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3488
3489                    else:
3490                        continue
3491
3492    def CloseAllOrders(self) -> None:
3493        """
3494        Gets a list of open pending and stop orders and cancel it all.
3495        """
3496        rawOrders = self.RequestPendingOrders()
3497        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3498        lenOrders = len(allOrdersIDs)
3499
3500        rawStopOrders = self.RequestStopOrders()
3501        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3502        lenSOrders = len(allStopOrdersIDs)
3503
3504        if lenOrders > 0 or lenSOrders > 0:
3505            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3506
3507            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3508
3509        else:
3510            uLogger.info("Orders not found, nothing to cancel.")
3511
3512    def CloseAll(self, *args) -> None:
3513        """
3514        Close all available (not blocked) opened trades and orders.
3515
3516        Also, you can select one or more keywords case-insensitive:
3517        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3518
3519        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3520        """
3521        overview = self.Overview(show=False)  # get all open trades info
3522
3523        if len(args) == 0:
3524            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3525            self.CloseAllOrders()  # close all pending and stop orders
3526
3527            for iType in TKS_INSTRUMENTS:
3528                if iType != "Currencies":
3529                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3530
3531        else:
3532            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3533            lowerArgs = [x.lower() for x in args]
3534
3535            if "orders" in lowerArgs:
3536                self.CloseAllOrders()  # close all pending and stop orders
3537
3538            for iType in TKS_INSTRUMENTS:
3539                if iType.lower() in lowerArgs and iType != "Currencies":
3540                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3541
3542    def CloseAllByTicker(self, instrument: str) -> None:
3543        """
3544        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3545
3546        This method searches opened trade and orders of instrument throw all portfolio and then use
3547        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3548
3549        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3550
3551        :param instrument: string with ticker.
3552        """
3553        if instrument is None or not instrument:
3554            uLogger.error("Ticker name must be defined for using this method!")
3555            raise Exception("Ticker required")
3556
3557        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3558
3559        self._ticker = instrument  # try to set instrument as ticker
3560        self._figi = ""
3561
3562        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3563        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3564
3565        if limitAll and self.IsInLimitOrders(portfolio=overview):
3566            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3567            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3568
3569        if stopAll and self.IsInStopOrders(portfolio=overview):
3570            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3571            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3572
3573        if self.IsInPortfolio(portfolio=overview):
3574            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3575            self.CloseTrades(instruments=[instrument], portfolio=overview)
3576
3577    def CloseAllByFIGI(self, instrument: str) -> None:
3578        """
3579        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3580
3581        This method searches opened trade and orders of instrument throw all portfolio and then use
3582        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3583
3584        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3585
3586        :param instrument: string with FIGI id.
3587        """
3588        if instrument is None or not instrument:
3589            uLogger.error("FIGI id must be defined for using this method!")
3590            raise Exception("FIGI required")
3591
3592        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3593
3594        self._ticker = ""
3595        self._figi = instrument  # try to set instrument as FIGI id
3596
3597        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3598        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3599
3600        if limitAll and self.IsInLimitOrders(portfolio=overview):
3601            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3602            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3603
3604        if stopAll and self.IsInStopOrders(portfolio=overview):
3605            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3606            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3607
3608        if self.IsInPortfolio(portfolio=overview):
3609            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3610            self.CloseTrades(instruments=[instrument], portfolio=overview)
3611
3612    @staticmethod
3613    def ParseOrderParameters(operation, **inputParameters):
3614        """
3615        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3616
3617        :param operation: string "Buy" or "Sell".
3618        :param inputParameters: this is dict of strings that looks like this
3619               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3620               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3621               "prices" key: one or more prices to open limit-orders
3622               Counts of values in lots and prices lists must be equals!
3623        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3624        """
3625        # TODO: update order grid work with api v2
3626        pass
3627        # uLogger.debug("Input parameters: {}".format(inputParameters))
3628        #
3629        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3630        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3631        #     raise Exception("Incorrect value")
3632        #
3633        # if "l" in inputParameters.keys():
3634        #     inputParameters["lots"] = inputParameters.pop("l")
3635        #
3636        # if "p" in inputParameters.keys():
3637        #     inputParameters["prices"] = inputParameters.pop("p")
3638        #
3639        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3640        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3641        #     raise Exception("Incorrect value")
3642        #
3643        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3644        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3645        #
3646        # if len(lots) != len(prices):
3647        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3648        #     raise Exception("Incorrect value")
3649        #
3650        # uLogger.debug("Extracted parameters for orders:")
3651        # uLogger.debug("lots = {}".format(lots))
3652        # uLogger.debug("prices = {}".format(prices))
3653        #
3654        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3655        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3656        # uLogger.debug("Order parameters: {}".format(result))
3657        #
3658        # return result
3659
3660    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3661        """
3662        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3663
3664        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3665        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3666        """
3667        result = False
3668        msg = "Instrument not defined!"
3669
3670        if portfolio is None or not portfolio:
3671            portfolio = self.Overview(show=False)
3672
3673        if self._ticker:
3674            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3675            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3676
3677            for iType in TKS_INSTRUMENTS:
3678                for instrument in portfolio["stat"][iType]:
3679                    if instrument["ticker"] == self._ticker:
3680                        result = True
3681                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3682                        break
3683
3684        elif self._figi:
3685            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3686            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3687
3688            for iType in TKS_INSTRUMENTS:
3689                for instrument in portfolio["stat"][iType]:
3690                    if instrument["figi"] == self._figi:
3691                        result = True
3692                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3693                        break
3694
3695        else:
3696            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3697
3698        uLogger.debug(msg)
3699
3700        return result
3701
3702    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3703        """
3704        Returns instrument from the user's portfolio if it presents there.
3705        Instrument must be defined by `ticker` (highly priority) or `figi`.
3706
3707        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3708        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3709        """
3710        result = None
3711        msg = "Instrument not defined!"
3712
3713        if portfolio is None or not portfolio:
3714            portfolio = self.Overview(show=False)
3715
3716        if self._ticker:
3717            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3718            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3719
3720            for iType in TKS_INSTRUMENTS:
3721                for instrument in portfolio["stat"][iType]:
3722                    if instrument["ticker"] == self._ticker:
3723                        result = instrument
3724                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3725                        break
3726
3727        elif self._figi:
3728            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3729            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3730
3731            for iType in TKS_INSTRUMENTS:
3732                for instrument in portfolio["stat"][iType]:
3733                    if instrument["figi"] == self._figi:
3734                        result = instrument
3735                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3736                        break
3737
3738        else:
3739            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3740
3741        uLogger.debug(msg)
3742
3743        return result
3744
3745    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3746        """
3747        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3748
3749        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3750
3751        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3752        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3753        """
3754        result = False
3755        msg = "Instrument not defined!"
3756
3757        if portfolio is None or not portfolio:
3758            portfolio = self.Overview(show=False)
3759
3760        if self._ticker:
3761            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3762            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3763
3764            for instrument in portfolio["stat"]["orders"]:
3765                if instrument["ticker"] == self._ticker:
3766                    result = True
3767                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3768                    break
3769
3770        elif self._figi:
3771            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3772            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3773
3774            for instrument in portfolio["stat"]["orders"]:
3775                if instrument["figi"] == self._figi:
3776                    result = True
3777                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3778                    break
3779
3780        else:
3781            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3782
3783        uLogger.debug(msg)
3784
3785        return result
3786
3787    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3788        """
3789        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3790        Instrument must be defined by `ticker` (highly priority) or `figi`.
3791
3792        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3793
3794        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3795        :return: list with `orderID`s of limit orders.
3796        """
3797        result = []
3798        msg = "Instrument not defined!"
3799
3800        if portfolio is None or not portfolio:
3801            portfolio = self.Overview(show=False)
3802
3803        if self._ticker:
3804            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3805            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3806
3807            for instrument in portfolio["stat"]["orders"]:
3808                if instrument["ticker"] == self._ticker:
3809                    result.append(instrument["orderID"])
3810
3811            if result:
3812                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3813
3814        elif self._figi:
3815            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3816            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3817
3818            for instrument in portfolio["stat"]["orders"]:
3819                if instrument["figi"] == self._figi:
3820                    result.append(instrument["orderID"])
3821
3822            if result:
3823                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3824
3825        else:
3826            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3827
3828        uLogger.debug(msg)
3829
3830        return result
3831
3832    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3833        """
3834        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3835
3836        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3837
3838        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3839        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3840        """
3841        result = False
3842        msg = "Instrument not defined!"
3843
3844        if portfolio is None or not portfolio:
3845            portfolio = self.Overview(show=False)
3846
3847        if self._ticker:
3848            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3849            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3850
3851            for instrument in portfolio["stat"]["stopOrders"]:
3852                if instrument["ticker"] == self._ticker:
3853                    result = True
3854                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3855                    break
3856
3857        elif self._figi:
3858            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3859            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3860
3861            for instrument in portfolio["stat"]["stopOrders"]:
3862                if instrument["figi"] == self._figi:
3863                    result = True
3864                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3865                    break
3866
3867        else:
3868            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3869
3870        uLogger.debug(msg)
3871
3872        return result
3873
3874    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3875        """
3876        Returns list with all `orderID`s of opened stop orders for the instrument.
3877        Instrument must be defined by `ticker` (highly priority) or `figi`.
3878
3879        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3880
3881        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3882        :return: list with `orderID`s of stop orders.
3883        """
3884        result = []
3885        msg = "Instrument not defined!"
3886
3887        if portfolio is None or not portfolio:
3888            portfolio = self.Overview(show=False)
3889
3890        if self._ticker:
3891            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3892            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3893
3894            for instrument in portfolio["stat"]["stopOrders"]:
3895                if instrument["ticker"] == self._ticker:
3896                    result.append(instrument["orderID"])
3897
3898            if result:
3899                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3900
3901        elif self._figi:
3902            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3903            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3904
3905            for instrument in portfolio["stat"]["stopOrders"]:
3906                if instrument["figi"] == self._figi:
3907                    result.append(instrument["orderID"])
3908
3909            if result:
3910                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3911
3912        else:
3913            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3914
3915        uLogger.debug(msg)
3916
3917        return result
3918
3919    def RequestLimits(self) -> dict:
3920        """
3921        Method for obtaining the available funds for withdrawal for current `accountId`.
3922
3923        See also:
3924        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3925        - `OverviewLimits()` method
3926
3927        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3928                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3929                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3930                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3931        """
3932        if self.accountId is None or not self.accountId:
3933            uLogger.error("Variable `accountId` must be defined for using this method!")
3934            raise Exception("Account ID required")
3935
3936        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3937
3938        self.body = str({"accountId": self.accountId})
3939        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3940        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3941
3942        if self.moreDebug:
3943            uLogger.debug("Records about available funds for withdrawal successfully received")
3944
3945        return rawLimits
3946
3947    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3948        """
3949        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3950
3951        See also: `RequestLimits()`.
3952
3953        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3954        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3955        :return: dict with raw parsed data from server and some calculated statistics about it.
3956        """
3957        if self.accountId is None or not self.accountId:
3958            uLogger.error("Variable `accountId` must be defined for using this method!")
3959            raise Exception("Account ID required")
3960
3961        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3962
3963        view = {
3964            "rawLimits": rawLimits,
3965            "limits": {  # parsed data for every currency:
3966                "money": {  # this is an array of portfolio currency positions
3967                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3968                },
3969                "blocked": {  # this is an array of blocked currency
3970                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3971                },
3972                "blockedGuarantee": {  # this is locked money under collateral for futures
3973                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3974                },
3975            },
3976        }
3977
3978        # --- Prepare text table with limits in human-readable format:
3979        if show or onlyFiles:
3980            info = [
3981                "# Withdrawal limits\n\n",
3982                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3983                "* **Account ID:** [{}]\n".format(self.accountId),
3984            ]
3985
3986            if view["limits"]["money"]:
3987                info.extend([
3988                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3989                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3990                ])
3991
3992            else:
3993                info.append("\nNo withdrawal limits\n")
3994
3995            for curr in view["limits"]["money"].keys():
3996                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3997                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3998                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3999
4000                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4001                    "[{}]".format(curr),
4002                    "{:.2f}".format(view["limits"]["money"][curr]),
4003                    "{:.2f}".format(availableMoney),
4004                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4005                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4006                )
4007
4008                if curr == "rub":
4009                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4010
4011                else:
4012                    info.append(infoStr)
4013
4014            infoText = "".join(info)
4015
4016            if show and not onlyFiles:
4017                uLogger.info(infoText)
4018
4019            if self.withdrawalLimitsFile and (show or onlyFiles):
4020                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4021                    fH.write(infoText)
4022
4023                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4024
4025                if self.useHTMLReports:
4026                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4027                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4028                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4029
4030                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4031
4032        return view
4033
4034    def RequestAccounts(self) -> dict:
4035        """
4036        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4037
4038        See also:
4039        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4040        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4041        - `OverviewUserInfo()` method
4042
4043        :return: dict with raw data from server that contains accounts info. Example of dict:
4044                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4045                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4046                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4047                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4048        """
4049        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4050
4051        self.body = str({})
4052        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4053        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4054
4055        if self.moreDebug:
4056            uLogger.debug("Records about available accounts successfully received")
4057
4058        return rawAccounts
4059
4060    def RequestUserInfo(self) -> dict:
4061        """
4062        Method for requesting common user's information.
4063
4064        See also:
4065        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4066        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4067        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4068        - `OverviewUserInfo()` method
4069
4070        :return: dict with raw data from server that contains user's information. Example of dict:
4071                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4072                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4073        """
4074        uLogger.debug("Requesting common user's information. Wait, please...")
4075
4076        self.body = str({})
4077        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4078        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4079
4080        if self.moreDebug:
4081            uLogger.debug("Records about current user successfully received")
4082
4083        return rawUserInfo
4084
4085    def RequestMarginStatus(self, accountId: str = None) -> dict:
4086        """
4087        Method for requesting margin calculation for defined account ID.
4088
4089        See also:
4090        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4091        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4092        - `OverviewUserInfo()` method
4093
4094        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4095        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4096                 Example of responses:
4097                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4098                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4099                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4100                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4101                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4102                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4103        """
4104        if accountId is None or not accountId:
4105            if self.accountId is None or not self.accountId:
4106                uLogger.error("Variable `accountId` must be defined for using this method!")
4107                raise Exception("Account ID required")
4108
4109            else:
4110                accountId = self.accountId  # use `self.accountId` (main ID) by default
4111
4112        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4113
4114        self.body = str({"accountId": accountId})
4115        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4116        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4117
4118        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4119            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4120            rawMargin = {}
4121
4122        else:
4123            if self.moreDebug:
4124                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4125
4126        return rawMargin
4127
4128    def RequestTariffLimits(self) -> dict:
4129        """
4130        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4131
4132        See also:
4133        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4134        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4135        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4136        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4137        - `OverviewUserInfo()` method
4138
4139        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4140                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4141                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4142        """
4143        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4144
4145        self.body = str({})
4146        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4147        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4148
4149        if self.moreDebug:
4150            uLogger.debug("Records with limits of current tariff successfully received")
4151
4152        return rawTariffLimits
4153
4154    def RequestBondCoupons(self, iJSON: dict) -> dict:
4155        """
4156        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4157        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4158        All dates are in UTC timezone.
4159
4160        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4161        Documentation:
4162        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4163        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4164
4165        See also: `ExtendBondsData()`.
4166
4167        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4168                      If raw iJSON is not data of bond then server returns an error [400] with message:
4169                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4170        :return: dictionary with bond payment calendar. Response example
4171                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4172                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4173                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4174                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4175        """
4176        if iJSON["figi"] is None or not iJSON["figi"]:
4177            uLogger.error("FIGI must be defined for using this method!")
4178            raise Exception("FIGI required")
4179
4180        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4181        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4182
4183        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4184            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4185            self._figi,
4186            startDate,
4187            endDate,
4188        ))
4189
4190        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4191        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4192        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4193
4194        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4195            uLogger.warning("Instrument type is not bond!")
4196
4197        else:
4198            if self.moreDebug:
4199                uLogger.debug("Records about bond payment calendar successfully received")
4200
4201        return calendar
4202
4203    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4204        """
4205        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4206        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4207        coupon yields, current yields and some statistics etc.
4208
4209        WARNING! This is too long operation if a lot of bonds requested from broker server.
4210
4211        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4212
4213        :param instruments: list of strings with tickers or FIGIs.
4214        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4215                     for further used by data scientists or stock analytics.
4216        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4217                 In XLSX-file and Pandas DataFrame fields mean:
4218                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4219                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4220        """
4221        if instruments is None or not instruments:
4222            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4223            raise Exception("Ticker or FIGI required")
4224
4225        if isinstance(instruments, str):
4226            instruments = [instruments]
4227
4228        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4229
4230        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4231
4232        iCount = len(uniqueInstruments)
4233        tooLong = iCount >= 20
4234        if tooLong:
4235            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4236
4237        bonds = None
4238        for i, self._figi in enumerate(uniqueInstruments):
4239            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4240
4241            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4242                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4243                rawBond = self.SearchByFIGI(requestPrice=True)
4244
4245                # Widen raw data with UTC current time (iData["actualDateTime"]):
4246                actualDate = datetime.now(tzutc())
4247                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4248
4249                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4250                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4251
4252                # Replace some values with human-readable:
4253                iData["nominalCurrency"] = iData["nominal"]["currency"]
4254                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4255                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4256                iData["aciCurrency"] = iData["aciValue"]["currency"]
4257                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4258                iData["issueSize"] = int(iData["issueSize"])
4259                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4260                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4261                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4262                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4263                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4264                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4265                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4266                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4267                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4268                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4269
4270                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4271                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4272                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4273                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4274                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4275                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4276                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4277                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4278                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4279                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4280                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4281
4282                # Widen raw data with calendar data from `rawCalendar` values:
4283                calendarData = []
4284                if "events" in iData["rawCalendar"].keys():
4285                    for item in iData["rawCalendar"]["events"]:
4286                        calendarData.append({
4287                            "couponDate": item["couponDate"],
4288                            "couponNumber": int(item["couponNumber"]),
4289                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4290                            "payCurrency": item["payOneBond"]["currency"],
4291                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4292                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4293                            "couponStartDate": item["couponStartDate"],
4294                            "couponEndDate": item["couponEndDate"],
4295                            "couponPeriod": item["couponPeriod"],
4296                        })
4297
4298                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4299                    if "maturityDate" not in iData.keys():
4300                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4301
4302                # Widen raw data with Coupon Rate.
4303                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4304                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4305                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4306                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4307
4308                # Widen raw data with Yield to Maturity (YTM) on current date.
4309                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4310                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4311                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4312                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4313                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4314                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4315
4316                iData["calendar"] = calendarData  # adds calendar at the end
4317
4318                # Remove not used data:
4319                iData.pop("uid")
4320                iData.pop("positionUid")
4321                iData.pop("currentPrice")
4322                iData.pop("rawCalendar")
4323
4324                colNames = list(iData.keys())
4325                if bonds is None:
4326                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4327
4328                else:
4329                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4330
4331            else:
4332                uLogger.warning("Instrument is not a bond!")
4333
4334            processed = round(100 * (i + 1) / iCount, 1)
4335            if tooLong and processed % 5 == 0:
4336                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4337
4338            else:
4339                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4340
4341        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4342
4343        # Saving bonds from Pandas DataFrame to XLSX sheet:
4344        if xlsx and self.bondsXLSXFile:
4345            with pd.ExcelWriter(
4346                    path=self.bondsXLSXFile,
4347                    date_format=TKS_DATE_FORMAT,
4348                    datetime_format=TKS_DATE_TIME_FORMAT,
4349                    mode="w",
4350            ) as writer:
4351                bonds.to_excel(
4352                    writer,
4353                    sheet_name="Extended bonds data",
4354                    index=True,
4355                    encoding="UTF-8",
4356                    freeze_panes=(1, 1),
4357                )  # saving as XLSX-file with freeze first row and column as headers
4358
4359            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4360
4361        return bonds
4362
4363    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4364        """
4365        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4366
4367        WARNING! This is too long operation if a lot of bonds requested from broker server.
4368
4369        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4370
4371        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4372                        extended information about bonds: main info, current prices, bond payment calendar,
4373                        coupon yields, current yields and some statistics etc.
4374                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4375        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4376                     for further used by data scientists or stock analytics.
4377        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4378        """
4379        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4380            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4381
4382        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4383
4384        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4385        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4386        calendar = None
4387        for bond in extBonds.iterrows():
4388            for item in bond[1]["calendar"]:
4389                cData = {
4390                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4391                    "couponDate": item["couponDate"],
4392                    "figi": bond[1]["figi"],
4393                    "ticker": bond[1]["ticker"],
4394                    "name": bond[1]["name"],
4395                    "couponNumber": item["couponNumber"],
4396                    "payOneBond": item["payOneBond"],
4397                    "payCurrency": item["payCurrency"],
4398                    "couponType": item["couponType"],
4399                    "couponPeriod": item["couponPeriod"],
4400                    "fixDate": item["fixDate"],
4401                    "couponStartDate": item["couponStartDate"],
4402                    "couponEndDate": item["couponEndDate"],
4403                }
4404
4405                if calendar is None:
4406                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4407
4408                else:
4409                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4410
4411        if calendar is not None:
4412            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4413
4414            # Saving calendar from Pandas DataFrame to XLSX sheet:
4415            if xlsx:
4416                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4417
4418                with pd.ExcelWriter(
4419                        path=xlsxCalendarFile,
4420                        date_format=TKS_DATE_FORMAT,
4421                        datetime_format=TKS_DATE_TIME_FORMAT,
4422                        mode="w",
4423                ) as writer:
4424                    humanReadable = calendar.copy(deep=True)
4425                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4426                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4427                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4428                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4429                    humanReadable.columns = colNames  # human-readable column names
4430
4431                    humanReadable.to_excel(
4432                        writer,
4433                        sheet_name="Bond payments calendar",
4434                        index=False,
4435                        encoding="UTF-8",
4436                        freeze_panes=(1, 2),
4437                    )  # saving as XLSX-file with freeze first row and column as headers
4438
4439                    del humanReadable  # release df in memory
4440
4441                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4442
4443        return calendar
4444
4445    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4446        """
4447        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4448        Also, creates Markdown file with calendar data, `calendar.md` by default.
4449
4450        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4451
4452        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4453                        extended information about bonds: main info, current prices, bond payment calendar,
4454                        coupon yields, current yields and some statistics etc.
4455                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4456        :param show: if `True` then also printing bonds payment calendar to the console,
4457                     otherwise save to file `calendarFile` only. `False` by default.
4458        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4459        :return: multilines text in Markdown format with bonds payment calendar as a table.
4460        """
4461        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4462            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4463
4464        infoText = "# Bond payments calendar\n\n"
4465
4466        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4467
4468        if not (calendar is None or calendar.empty):
4469            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4470
4471            info = [
4472                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4473                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4474                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4475            ]
4476
4477            newMonth = False
4478            notOneBond = calendar["figi"].nunique() > 1
4479            for i, bond in enumerate(calendar.iterrows()):
4480                if newMonth and notOneBond:
4481                    info.append(splitLine)
4482
4483                info.append(
4484                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4485                        "  √" if bond[1]["paid"] else "  —",
4486                        bond[1]["couponDate"].split("T")[0],
4487                        bond[1]["figi"],
4488                        bond[1]["ticker"],
4489                        bond[1]["couponNumber"],
4490                        "{} {}".format(
4491                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4492                            bond[1]["payCurrency"],
4493                        ),
4494                        bond[1]["couponType"],
4495                        bond[1]["couponPeriod"],
4496                        bond[1]["fixDate"].split("T")[0],
4497                    )
4498                )
4499
4500                if i < len(calendar.values) - 1:
4501                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4502                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4503                    newMonth = False if curDate.month == nextDate.month else True
4504
4505                else:
4506                    newMonth = False
4507
4508            infoText += "".join(info)
4509
4510            if show and not onlyFiles:
4511                uLogger.info("{}".format(infoText))
4512
4513            if self.calendarFile is not None and (show or onlyFiles):
4514                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4515                    fH.write(infoText)
4516
4517                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4518
4519                if self.useHTMLReports:
4520                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4521                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4522                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4523
4524                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4525
4526        else:
4527            infoText += "No data\n"
4528
4529        return infoText
4530
4531    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4532        """
4533        Method for parsing and show simple table with all available user accounts.
4534
4535        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4536
4537        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4538        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4539        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4540                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4541                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4542                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4543                                                        "closed": "—", "access": "Full access" }, ...}}`
4544        """
4545        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4546
4547        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4548        accounts = {
4549            item["id"]: {
4550                "type": TKS_ACCOUNT_TYPES[item["type"]],
4551                "name": item["name"],
4552                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4553                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4554                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4555                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4556            } for item in rawAccounts["accounts"]
4557        }
4558
4559        # Raw and parsed data with some fields replaced in "stat" section:
4560        view = {
4561            "rawAccounts": rawAccounts,
4562            "stat": accounts,
4563        }
4564
4565        # --- Prepare simple text table with only accounts data in human-readable format:
4566        if show or onlyFiles:
4567            info = [
4568                "# User accounts\n\n",
4569                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4570                "| Account ID   | Type                      | Status                    | Name                           |\n",
4571                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4572            ]
4573
4574            for account in view["stat"].keys():
4575                info.extend([
4576                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4577                        account,
4578                        view["stat"][account]["type"],
4579                        view["stat"][account]["status"],
4580                        view["stat"][account]["name"],
4581                    )
4582                ])
4583
4584            infoText = "".join(info)
4585
4586            if show and not onlyFiles:
4587                uLogger.info(infoText)
4588
4589            if self.userAccountsFile and (show or onlyFiles):
4590                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4591                    fH.write(infoText)
4592
4593                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4594
4595                if self.useHTMLReports:
4596                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4597                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4598                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4599
4600                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4601
4602        return view
4603
4604    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4605        """
4606        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4607
4608        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4609
4610        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4611        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4612        :return: dict with raw parsed data from server and some calculated statistics about it.
4613        """
4614        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4615        tmpTicker = self._ticker
4616        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4617        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4618        self._ticker = tmpTicker
4619
4620        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4621        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4622        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4623        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4624        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4625        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4626
4627        # This is dict with parsed common user data:
4628        userInfo = {
4629            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4630            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4631            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4632            "tariff": rawUserInfo["tariff"],
4633        }
4634
4635        # This is an array of dict with parsed margin statuses for every account IDs:
4636        margins = {}
4637        for accountId in accounts.keys():
4638            if rawMargins[accountId]:
4639                margins[accountId] = {
4640                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4641                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4642                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4643                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4644                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4645                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4646                    "missing": missing["volume"],
4647                }
4648
4649            else:
4650                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4651
4652        unary = {}  # unary-connection limits
4653        for item in rawTariffLimits["unaryLimits"]:
4654            if item["limitPerMinute"] in unary.keys():
4655                unary[item["limitPerMinute"]].extend(item["methods"])
4656
4657            else:
4658                unary[item["limitPerMinute"]] = item["methods"]
4659
4660        stream = {}  # stream-connection limits
4661        for item in rawTariffLimits["streamLimits"]:
4662            if item["limit"] in stream.keys():
4663                stream[item["limit"]].extend(item["streams"])
4664
4665            else:
4666                stream[item["limit"]] = item["streams"]
4667
4668        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4669        limits = {
4670            "unary": unary,
4671            "stream": stream,
4672        }
4673
4674        # Raw and parsed data as an output result:
4675        view = {
4676            "rawUserInfo": rawUserInfo,
4677            "rawAccounts": rawAccounts,
4678            "rawMargins": rawMargins,
4679            "rawTariffLimits": rawTariffLimits,
4680            "stat": {
4681                "overview": overview,
4682                "userInfo": userInfo,
4683                "accounts": accounts,
4684                "margins": margins,
4685                "limits": limits,
4686            },
4687        }
4688
4689        # --- Prepare text table with user information in human-readable format:
4690        if show or onlyFiles:
4691            info = [
4692                "# Full user information\n\n",
4693                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4694                "## Common information\n\n",
4695                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4696                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4697                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4698                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4699                "\n## User accounts\n\n",
4700            ]
4701
4702            for account in view["stat"]["accounts"].keys():
4703                info.extend([
4704                    "### ID: [{}]\n\n".format(account),
4705                    "| Parameters           | Values                                                       |\n",
4706                    "|----------------------|--------------------------------------------------------------|\n",
4707                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4708                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4709                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4710                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4711                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4712                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4713                ])
4714
4715                if margins[account]:
4716                    info.extend([
4717                        "| Margin status:       | Enabled                                                      |\n",
4718                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4719                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4720                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4721                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4722                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4723                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4724                    ])
4725
4726                else:
4727                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4728
4729            info.extend([
4730                "\n## Current user tariff limits\n",
4731                "\n### See also\n",
4732                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4733                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4734                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4735                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4736                "\n### Unary limits\n",
4737            ])
4738
4739            if unary:
4740                for key, values in sorted(unary.items()):
4741                    info.append("\n* Max requests per minute: {}\n".format(key))
4742
4743                    for value in values:
4744                        info.append("  - {}\n".format(value))
4745
4746            else:
4747                info.append("\nNot available\n")
4748
4749            info.append("\n### Stream limits\n")
4750
4751            if stream:
4752                for key, values in sorted(stream.items()):
4753                    info.append("\n* Max stream connections: {}\n".format(key))
4754
4755                    for value in values:
4756                        info.append("  - {}\n".format(value))
4757
4758            else:
4759                info.append("\nNot available\n")
4760
4761            infoText = "".join(info)
4762
4763            if show and not onlyFiles:
4764                uLogger.info(infoText)
4765
4766            if self.userInfoFile and (show or onlyFiles):
4767                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4768                    fH.write(infoText)
4769
4770                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4771
4772                if self.useHTMLReports:
4773                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4774                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4775                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4776
4777                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4778
4779        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self._tag = ""
130        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
131
132        self.__lock = Lock()  # initialize multiprocessing mutex lock
133
134        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
135
136        self.aliases = TKS_TICKER_ALIASES
137        """Some aliases instead official tickers.
138
139        See also: `TKSEnums.TKS_TICKER_ALIASES`
140        """
141
142        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
143
144        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
145
146        self._ticker = ""
147        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
148
149        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
150        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
151
152        See also: `SearchByTicker()`, `SearchInstruments()`.
153        """
154
155        self._figi = ""
156        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
157
158        See also: `SearchByFIGI()`, `SearchInstruments()`.
159        """
160
161        self.depth = 1
162        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
163
164        See also: `GetCurrentPrices()`.
165        """
166
167        self.server = r"https://invest-public-api.tinkoff.ru/rest"
168        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
169
170        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
171        """
172
173        uLogger.debug("Broker API server: {}".format(self.server))
174
175        self.timeout = 15
176        """Server operations timeout in seconds. Default: `15`.
177
178        See also: `SendAPIRequest()`.
179        """
180
181        self.headers = {
182            "Content-Type": "application/json",
183            "accept": "application/json",
184            "Authorization": "Bearer {}".format(self.token),
185            "x-app-name": "Tim55667757.TKSBrokerAPI",
186        }
187        """
188        Headers which send in every request to broker server. Please, do not change it!
189        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
190
191        See also: `SendAPIRequest()`.
192        """
193
194        self.body = None
195        """Request body which send to broker server. Default: `None`.
196
197        See also: `SendAPIRequest()`.
198        """
199
200        self.moreDebug = False
201        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
202
203        self.useHTMLReports = False
204        """
205        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
206        
207        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
208        """
209
210        self.historyFile = None
211        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
212
213        See also: `History()`.
214        """
215
216        self.htmlHistoryFile = "index.html"
217        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
218
219        See also: `ShowHistoryChart()`.
220        """
221
222        self.instrumentsFile = "instruments.md"
223        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
224
225        See also: `ShowInstrumentsInfo()`.
226        """
227
228        self.searchResultsFile = "search-results.md"
229        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
230
231        See also: `SearchInstruments()`.
232        """
233
234        self.pricesFile = "prices.md"
235        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
236
237        See also: `GetListOfPrices()`.
238        """
239
240        self.infoFile = "info.md"
241        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
242
243        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
244        """
245
246        self.bondsXLSXFile = "ext-bonds.xlsx"
247        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
248        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
249
250        See also: `ExtendBondsData()`.
251        """
252
253        self.calendarFile = "calendar.md"
254        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
255        
256        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
257
258        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
259        """
260
261        self.overviewFile = "overview.md"
262        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
263
264        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
265        """
266
267        self.overviewDigestFile = "overview-digest.md"
268        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
269
270        See also: `Overview()` with parameter `details="digest"`.
271        """
272
273        self.overviewPositionsFile = "overview-positions.md"
274        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
275
276        See also: `Overview()` with parameter `details="positions"`.
277        """
278
279        self.overviewOrdersFile = "overview-orders.md"
280        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
281
282        See also: `Overview()` with parameter `details="orders"`.
283        """
284
285        self.overviewAnalyticsFile = "overview-analytics.md"
286        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
287
288        See also: `Overview()` with parameter `details="analytics"`.
289        """
290
291        self.overviewBondsCalendarFile = "overview-calendar.md"
292        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
293
294        See also: `Overview()` with parameter `details="calendar"`.
295        """
296
297        self.reportFile = "deals.md"
298        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
299
300        See also: `Deals()`.
301        """
302
303        self.withdrawalLimitsFile = "limits.md"
304        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
305
306        See also: `OverviewLimits()` and `RequestLimits()`.
307        """
308
309        self.userInfoFile = "user-info.md"
310        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
311
312        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
313        """
314
315        self.userAccountsFile = "accounts.md"
316        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
317
318        See also: `OverviewAccounts()`, `RequestAccounts()`.
319        """
320
321        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
322        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
323
324        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
325
326        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
327        """
328
329        self.iList = None  # init iList for raw instruments data
330        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
331        
332        See also: `Listing()`, `DumpInstruments()`.
333        """
334
335        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
336        if useCache:
337            if os.path.exists(self.iListDumpFile):
338                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
339                curTime = datetime.now(tzutc())
340
341                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
342                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
343
344                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
345
346                else:
347                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
348
349                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
350                        os.path.abspath(self.iListDumpFile),
351                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
352                    ))
353
354            else:
355                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
356                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
357
358        else:
359            self.iList = self.Listing()  # request new raw instruments data from broker server
360            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
361
362        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
363        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
364
365        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
366        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

tag: str

Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: "" (empty string).

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
448    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
449        """
450        Send GET or POST request to broker server and receive JSON object.
451
452        self.header: must be defining with dictionary of headers.
453        self.body: if define then used as request body. None by default.
454        self.timeout: global request timeout, 15 seconds by default.
455        :param url: url with REST request.
456        :param reqType: send "GET" or "POST" request. "GET" by default.
457        :param retry: how many times retry after first request if an 5xx server errors occurred.
458        :param pause: sleep time in seconds between retries.
459        :return: response JSON (dictionary) from broker.
460        """
461        if reqType.upper() not in ("GET", "POST"):
462            uLogger.error("You can define request type: `GET` or `POST`!")
463            raise Exception("Incorrect value")
464
465        if self.moreDebug:
466            uLogger.debug("Request parameters:")
467            uLogger.debug("    - REST API URL: {}".format(url))
468            uLogger.debug("    - request type: {}".format(reqType))
469            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
470            uLogger.debug("    - body:\n{}".format(self.body))
471
472        # fast hack to avoid all operations with some tickers/FIGI
473        responseJSON = {}
474        oK = True
475        for item in self.exclude:
476            if item in url:
477                if self.moreDebug:
478                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
479
480                oK = False
481                break
482
483        if oK:
484            with self.__lock:  # acquire the mutex lock
485                counter = 0
486                response = None
487                errMsg = ""
488
489                while not response and counter <= retry:
490                    if reqType == "GET":
491                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
492
493                    if reqType == "POST":
494                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
495
496                    if self.moreDebug:
497                        uLogger.debug("Response:")
498                        uLogger.debug("    - status code: {}".format(response.status_code))
499                        uLogger.debug("    - reason: {}".format(response.reason))
500                        uLogger.debug("    - body length: {}".format(len(response.text)))
501                        uLogger.debug("    - headers:\n{}".format(response.headers))
502
503                    # Server returns some headers:
504                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
505                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
506                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
507                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
508                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
509                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
510                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
511                        sleep(rateLimitWait)
512
513                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
514                    if 400 <= response.status_code < 500:
515                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
516                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
517
518                        if "code" in response.text and "message" in response.text:
519                            msgDict = self._ParseJSON(rawData=response.text)
520                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
521
522                        counter = retry + 1  # do not retry for 4xx errors
523
524                    if 500 <= response.status_code < 600:
525                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
526                        uLogger.debug("    - not oK, {}".format(errMsg))
527
528                        if "code" in response.text and "message" in response.text:
529                            errMsgDict = self._ParseJSON(rawData=response.text)
530                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
531
532                        counter += 1
533
534                        if counter <= retry:
535                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
536                            sleep(pause)
537
538                responseJSON = self._ParseJSON(rawData=response.text)
539
540                if errMsg:
541                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
542                    uLogger.error("    - not oK, {}".format(errMsg))
543
544        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
577    def Listing(self) -> dict:
578        """
579        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
580
581        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
582        """
583        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
584        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
585
586        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
587        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
588        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
589
590        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
591        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
592        poolUpdater.close()  # close the thread pool
593        poolUpdater.join()  # wait a moment until all data returns from threads
594
595        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
596        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
597        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
598
599        # calculate minimum price increment (step) for all instruments and set up instrument's type:
600        for iType in iList.keys():
601            for ticker in iList[iType]:
602                iList[iType][ticker]["type"] = iType
603
604                if "minPriceIncrement" in iList[iType][ticker].keys():
605                    iList[iType][ticker]["step"] = NanoToFloat(
606                        iList[iType][ticker]["minPriceIncrement"]["units"],
607                        iList[iType][ticker]["minPriceIncrement"]["nano"],
608                    )
609
610                else:
611                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
612
613        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
615    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
616        """
617        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
618
619        See also: `DumpInstruments()`, `Listing()`.
620
621        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
622                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
623        """
624        if self.iListDumpFile is None or not self.iListDumpFile:
625            uLogger.error("Output name of dump file must be defined!")
626            raise Exception("Filename required")
627
628        if not self.iList or forceUpdate:
629            self.iList = self.Listing()
630
631        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
632
633        # Save as XLSX with separated sheets for every type of instruments:
634        with pd.ExcelWriter(
635                path=xlsxDumpFile,
636                date_format=TKS_DATE_FORMAT,
637                datetime_format=TKS_DATE_TIME_FORMAT,
638                mode="w",
639        ) as writer:
640            for iType in TKS_INSTRUMENTS:
641                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
642                df = df[sorted(df)]  # sorted by column names
643                df = df.applymap(
644                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
645                    na_action="ignore",
646                )  # converting numbers from nano-type to float in every cell
647                df.to_excel(
648                    writer,
649                    sheet_name=iType,
650                    encoding="UTF-8",
651                    freeze_panes=(1, 1),
652                )  # saving as XLSX-file with freeze first row and column as headers
653
654        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
656    def DumpInstruments(self, forceUpdate: bool = True) -> str:
657        """
658        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
659        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
660
661        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
662
663        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
664                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
665        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
666        """
667        if self.iListDumpFile is None or not self.iListDumpFile:
668            uLogger.error("Output name of dump file must be defined!")
669            raise Exception("Filename required")
670
671        if not self.iList or forceUpdate:
672            self.iList = self.Listing()
673
674        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
675        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
676            fH.write(jsonDump)
677
678        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
679
680        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
682    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
683        """
684        Show information about one instrument defined by json data and prints it in Markdown format.
685
686        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
687
688        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
689        :param show: if `True` then also printing information about instrument and its current price.
690        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
691        :return: multilines text in Markdown format with information about one instrument.
692        """
693        splitLine = "|                                                             |                                                        |\n"
694        infoText = ""
695
696        if iJSON is not None and iJSON and isinstance(iJSON, dict):
697            info = [
698                "# Main information\n\n",
699                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
700                "| Parameters                                                  | Values                                                 |\n",
701                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
702                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
703                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
704            ]
705
706            if "sector" in iJSON.keys() and iJSON["sector"]:
707                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
708
709            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
710                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
711
712            info.extend([
713                splitLine,
714                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
715                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
716            ])
717
718            if "isin" in iJSON.keys() and iJSON["isin"]:
719                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
720
721            if "classCode" in iJSON.keys():
722                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
723
724            info.extend([
725                splitLine,
726                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
727                splitLine,
728                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
729                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
730                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
731            ])
732
733            if iJSON["figi"]:
734                self._figi = iJSON["figi"]
735                iJSON = iJSON | self.RequestTradingStatus()
736
737                info.extend([
738                    splitLine,
739                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
740                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
741                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
742                ])
743
744            info.append(splitLine)
745
746            if "type" in iJSON.keys() and iJSON["type"]:
747                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
748
749                if "shareType" in iJSON.keys() and iJSON["shareType"]:
750                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
751
752            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
753                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
754
755            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
756                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
757
758            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
759                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
760
761            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
762                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
763
764            if "focusType" in iJSON.keys() and iJSON["focusType"]:
765                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
766
767            if "assetType" in iJSON.keys() and iJSON["assetType"]:
768                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
769
770            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
771                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
772
773            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
774                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
775
776            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
777                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
778
779            if "currency" in iJSON.keys():
780                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
781
782            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
783                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
784
785            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
786                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
787
788            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
789                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
790
791            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
792                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
793
794            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
795                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
796
797            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
798                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
799
800            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
801                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
802
803            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
804                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
805
806            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
807                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
808
809            iExt = None
810            if iJSON["type"] == "Bonds":
811                info.extend([
812                    splitLine,
813                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
814                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
815                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
816                        iJSON["nominal"]["currency"],
817                    )),
818                ])
819
820                if "floatingCouponFlag" in iJSON.keys():
821                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
822
823                if "amortizationFlag" in iJSON.keys():
824                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
825
826                info.append(splitLine)
827
828                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
829                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
830
831                if iJSON["figi"]:
832                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
833
834                    info.extend([
835                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
836                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
837                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
838                    ])
839
840                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
841                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
842                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
843                        iJSON["aciValue"]["currency"]
844                    )))
845
846            if "currentPrice" in iJSON.keys():
847                info.append(splitLine)
848
849                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
850                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
851
852                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
853                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
854                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
855                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
856                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
857
858                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
859                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
860
861                info.extend([
862                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
863                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
864                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
865                    )),
866                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
867                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
868                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
869                    )),
870                    "| Changes between last deal price and last close              | {:<54} |\n".format(
871                        "{:.2f}%{}".format(
872                            iJSON["currentPrice"]["changes"],
873                            " ({}{:.2f} {})".format(
874                                "+" if bondChangesDelta > 0 else "",
875                                bondChangesDelta,
876                                aciCurrency
877                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
878                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
879                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
880                                currency
881                            ),
882                        )
883                    ),
884                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
885                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
886                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
887                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
888                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
889                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
890                    )),
891                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
892                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
893                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
894                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
895                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
896                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
897                    )),
898                ])
899
900            if "lot" in iJSON.keys():
901                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
902
903            if "step" in iJSON.keys() and iJSON["step"] != 0:
904                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
905
906            # Add bond payment calendar:
907            if iJSON["type"] == "Bonds":
908                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
909                info.extend(["\n#", strCalendar])
910
911            infoText += "".join(info)
912
913            if show and not onlyFiles:
914                uLogger.info("{}".format(infoText))
915
916            if self.infoFile is not None and (show or onlyFiles):
917                with open(self.infoFile, "w", encoding="UTF-8") as fH:
918                    fH.write(infoText)
919
920                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
921
922                if self.useHTMLReports:
923                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
924                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
925                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
926
927                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
928
929        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
931    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
932        """
933        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
934
935        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
936        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
937        :return: JSON formatted data with information about instrument.
938        """
939        tickerJSON = {}
940        if self.moreDebug:
941            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
942
943        if not self._ticker:
944            uLogger.warning("self._ticker variable is not be empty!")
945
946        else:
947            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
948                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
949                raise Exception("Instrument not allowed")
950
951            if not self.iList:
952                self.iList = self.Listing()
953
954            if self._ticker in self.iList["Shares"].keys():
955                tickerJSON = self.iList["Shares"][self._ticker]
956                if self.moreDebug:
957                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
958
959            elif self._ticker in self.iList["Currencies"].keys():
960                tickerJSON = self.iList["Currencies"][self._ticker]
961                if self.moreDebug:
962                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
963
964            elif self._ticker in self.iList["Bonds"].keys():
965                tickerJSON = self.iList["Bonds"][self._ticker]
966                if self.moreDebug:
967                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
968
969            elif self._ticker in self.iList["Etfs"].keys():
970                tickerJSON = self.iList["Etfs"][self._ticker]
971                if self.moreDebug:
972                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
973
974            elif self._ticker in self.iList["Futures"].keys():
975                tickerJSON = self.iList["Futures"][self._ticker]
976                if self.moreDebug:
977                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
978
979        if tickerJSON:
980            self._figi = tickerJSON["figi"]
981
982            if requestPrice:
983                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
984
985                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
986                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
987
988                else:
989                    tickerJSON["currentPrice"]["changes"] = 0
990
991            if show:
992                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
993
994        else:
995            if show:
996                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
997
998        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1000    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1001        """
1002        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1003
1004        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1005        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1006        :return: JSON formatted data with information about instrument.
1007        """
1008        figiJSON = {}
1009        if self.moreDebug:
1010            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1011
1012        if not self._figi:
1013            uLogger.warning("self._figi variable is not be empty!")
1014
1015        else:
1016            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1017                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1018                raise Exception("Instrument not allowed")
1019
1020            if not self.iList:
1021                self.iList = self.Listing()
1022
1023            for item in self.iList["Shares"].keys():
1024                if self._figi == self.iList["Shares"][item]["figi"]:
1025                    figiJSON = self.iList["Shares"][item]
1026
1027                    if self.moreDebug:
1028                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1029
1030                    break
1031
1032            if not figiJSON:
1033                for item in self.iList["Currencies"].keys():
1034                    if self._figi == self.iList["Currencies"][item]["figi"]:
1035                        figiJSON = self.iList["Currencies"][item]
1036
1037                        if self.moreDebug:
1038                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1039
1040                        break
1041
1042            if not figiJSON:
1043                for item in self.iList["Bonds"].keys():
1044                    if self._figi == self.iList["Bonds"][item]["figi"]:
1045                        figiJSON = self.iList["Bonds"][item]
1046
1047                        if self.moreDebug:
1048                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1049
1050                        break
1051
1052            if not figiJSON:
1053                for item in self.iList["Etfs"].keys():
1054                    if self._figi == self.iList["Etfs"][item]["figi"]:
1055                        figiJSON = self.iList["Etfs"][item]
1056
1057                        if self.moreDebug:
1058                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1059
1060                        break
1061
1062            if not figiJSON:
1063                for item in self.iList["Futures"].keys():
1064                    if self._figi == self.iList["Futures"][item]["figi"]:
1065                        figiJSON = self.iList["Futures"][item]
1066
1067                        if self.moreDebug:
1068                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1069
1070                        break
1071
1072        if figiJSON:
1073            self._figi = figiJSON["figi"]
1074            self._ticker = figiJSON["ticker"]
1075
1076            if requestPrice:
1077                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1078
1079                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1080                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1081
1082                else:
1083                    figiJSON["currentPrice"]["changes"] = 0
1084
1085            if show:
1086                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1087
1088        else:
1089            if show:
1090                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1091
1092        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1094    def GetCurrentPrices(self, show: bool = True) -> dict:
1095        """
1096        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1097        `{"buy": [{"price": 1243.8, "quantity": 193},
1098                  {"price": 1244.0, "quantity": 168},
1099                  {"price": 1244.8, "quantity": 5},
1100                  {"price": 1245.0, "quantity": 61},
1101                  {"price": 1245.4, "quantity": 60}],
1102          "sell": [{"price": 1243.6, "quantity": 8},
1103                   {"price": 1242.6, "quantity": 10},
1104                   {"price": 1242.4, "quantity": 18},
1105                   {"price": 1242.2, "quantity": 50},
1106                   {"price": 1242.0, "quantity": 113}],
1107          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1108        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1109        - sell: list of dicts with Buyers prices,
1110            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1111            - quantity: volume value by current price in lots,
1112        - limitUp: current trade session limit price, maximum,
1113        - limitDown: current trade session limit price, minimum,
1114        - lastPrice: last deal price of the instrument,
1115        - closePrice: previous trade session close price of the instrument.
1116
1117        See also: `SearchByTicker()` and `SearchByFIGI()`.
1118        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1119        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1120
1121        :param show: if `True` then print DOM to log and console.
1122        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1123                 If an error occurred then returns an empty record:
1124                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1125        """
1126        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1127
1128        if self.depth < 1:
1129            uLogger.error("Depth of Market (DOM) must be >=1!")
1130            raise Exception("Incorrect value")
1131
1132        if not (self._ticker or self._figi):
1133            uLogger.error("self._ticker or self._figi variables must be defined!")
1134            raise Exception("Ticker or FIGI required")
1135
1136        if self._ticker and not self._figi:
1137            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1138            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1139
1140        if not self._ticker and self._figi:
1141            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1142            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1143
1144        if not self._figi:
1145            uLogger.error("FIGI is not defined!")
1146            raise Exception("Ticker or FIGI required")
1147
1148        else:
1149            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1150
1151            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1152            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1153            self.body = str({"figi": self._figi, "depth": self.depth})
1154            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1155
1156            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1157                # list of dicts with sellers orders:
1158                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1159
1160                # list of dicts with buyers orders:
1161                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1162
1163                # max price of instrument at this time:
1164                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1165
1166                # min price of instrument at this time:
1167                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1168
1169                # last price of deal with instrument:
1170                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1171
1172                # last close price of instrument:
1173                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1174
1175            else:
1176                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1177                uLogger.debug("Server response: {}".format(pricesResponse))
1178
1179            if show:
1180                if prices["buy"] or prices["sell"]:
1181                    info = [
1182                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1183                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1184                            self._ticker,
1185                            self._figi,
1186                            self.depth,
1187                        ),
1188                        "-" * 60, "\n",
1189                        "             Orders of Buyers | Orders of Sellers\n",
1190                        "-" * 60, "\n",
1191                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1192                        "-" * 60, "\n",
1193                    ]
1194
1195                    if not prices["buy"]:
1196                        info.append("                              | No orders!\n")
1197                        sumBuy = 0
1198
1199                    else:
1200                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1201                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1202                        for item in maxMinSorted:
1203                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1204
1205                    if not prices["sell"]:
1206                        info.append("No orders!                    |\n")
1207                        sumSell = 0
1208
1209                    else:
1210                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1211                        for item in prices["sell"]:
1212                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1213
1214                    info.extend([
1215                        "-" * 60, "\n",
1216                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1217                        "-" * 60, "\n",
1218                    ])
1219
1220                    infoText = "".join(info)
1221
1222                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1223
1224                else:
1225                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1226
1227        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1229    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1230        """
1231        This method get and show information about all available broker instruments for current user account.
1232        If `instrumentsFile` string is not empty then also save information to this file.
1233
1234        :param show: if `True` then print results to console, if `False` — print only to file.
1235        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1236        :return: multi-lines string with all available broker instruments.
1237        """
1238        if not self.iList:
1239            self.iList = self.Listing()
1240
1241        info = [
1242            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1243            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1244        ]
1245
1246        # add instruments count by type:
1247        for iType in self.iList.keys():
1248            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1249
1250        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1251        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1252
1253        # generating info tables with all instruments by type:
1254        for iType in self.iList.keys():
1255            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1256
1257            for instrument in self.iList[iType].keys():
1258                iName = self.iList[iType][instrument]["name"]  # instrument's name
1259                if len(iName) > 57:
1260                    iName = "{}...".format(iName[:54])  # right trim for a long string
1261
1262                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1263                    self.iList[iType][instrument]["ticker"],
1264                    iName,
1265                    self.iList[iType][instrument]["figi"],
1266                    self.iList[iType][instrument]["currency"],
1267                    self.iList[iType][instrument]["lot"],
1268                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1269                ))
1270
1271        infoText = "".join(info)
1272
1273        if show and not onlyFiles:
1274            uLogger.info(infoText)
1275
1276        if self.instrumentsFile and (show or onlyFiles):
1277            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1278                fH.write(infoText)
1279
1280            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1281
1282            if self.useHTMLReports:
1283                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1284                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1285                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1286
1287                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1288
1289        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multi-lines string with all available broker instruments.

def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1291    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1292        """
1293        This method search and show information about instruments by part of its ticker, FIGI or name.
1294        If `searchResultsFile` string is not empty then also save information to this file.
1295
1296        :param pattern: string with part of ticker, FIGI or instrument's name.
1297        :param show: if `True` then print results to console, if `False` — return list of result only.
1298        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1299        :return: list of dictionaries with all found instruments.
1300        """
1301        if not self.iList:
1302            self.iList = self.Listing()
1303
1304        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1305        compiledPattern = re.compile(pattern, re.IGNORECASE)
1306
1307        for iType in self.iList:
1308            for instrument in self.iList[iType].values():
1309                searchResult = compiledPattern.search(" ".join(
1310                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1311                ))
1312
1313                if searchResult:
1314                    searchResults[iType][instrument["ticker"]] = instrument
1315
1316        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1317        info = [
1318            "# Search results\n\n",
1319            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1320            "* **Search pattern:** [{}]\n".format(pattern),
1321            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1322            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1323        ]
1324        infoShort = info[:]
1325
1326        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1327        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1328        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1329
1330        if resultsLen == 0:
1331            info.append("\nNo results\n")
1332            infoShort.append("\nNo results\n")
1333            uLogger.warning("No results. Try changing your search pattern.")
1334
1335        else:
1336            for iType in searchResults:
1337                iTypeValuesCount = len(searchResults[iType].values())
1338                if iTypeValuesCount > 0:
1339                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1340                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1341
1342                    for instrument in searchResults[iType].values():
1343                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1344                            instrument["type"],
1345                            instrument["ticker"],
1346                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1347                            instrument["figi"],
1348                        ))
1349
1350                    if iTypeValuesCount <= 5:
1351                        infoShort.extend(info[-iTypeValuesCount:])
1352
1353                    else:
1354                        infoShort.extend(info[-5:])
1355                        infoShort.append(skippedLine)
1356
1357        infoText = "".join(info)
1358        infoTextShort = "".join(infoShort)
1359
1360        if show and not onlyFiles:
1361            uLogger.info(infoTextShort)
1362            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1363
1364        if self.searchResultsFile and (show or onlyFiles):
1365            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1366                fH.write(infoText)
1367
1368            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1369
1370            if self.useHTMLReports:
1371                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1372                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1373                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1374
1375                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1376
1377        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1379    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1380        """
1381        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1382
1383        :param instruments: list of strings with tickers or FIGIs.
1384        :return: list with unique instrument FIGIs only.
1385        """
1386        requestedInstruments = []
1387        for iName in instruments:
1388            if iName not in self.aliases.keys():
1389                if iName not in requestedInstruments:
1390                    requestedInstruments.append(iName)
1391
1392            else:
1393                if iName not in requestedInstruments:
1394                    if self.aliases[iName] not in requestedInstruments:
1395                        requestedInstruments.append(self.aliases[iName])
1396
1397        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1398
1399        onlyUniqueFIGIs = []
1400        for iName in requestedInstruments:
1401            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1402                continue
1403
1404            self._ticker = iName
1405            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1406
1407            if not iData:
1408                self._ticker = ""
1409                self._figi = iName
1410
1411                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1412
1413                if not iData:
1414                    self._figi = ""
1415                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1416
1417            if iData and iData["figi"] not in onlyUniqueFIGIs:
1418                onlyUniqueFIGIs.append(iData["figi"])
1419
1420        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1421
1422        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices( self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1424    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1425        """
1426        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1427
1428        See limits: https://tinkoff.github.io/investAPI/limits/
1429
1430        If `pricesFile` string is not empty then also save information to this file.
1431
1432        :param instruments: list of strings with tickers or FIGIs.
1433        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1434        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1435        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1436                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1437        """
1438        if instruments is None or not instruments:
1439            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1440            raise Exception("Ticker or FIGI required")
1441
1442        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1443
1444        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1445
1446        iList = []  # trying to get info and current prices about all unique instruments:
1447        for self._figi in onlyUniqueFIGIs:
1448            iData = self.SearchByFIGI(requestPrice=True, show=False)
1449            iList.append(iData)
1450
1451        self.ShowListOfPrices(iList, show, onlyFiles)
1452
1453        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1455    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1456        """
1457        Show table contains current prices of given instruments.
1458
1459        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1460                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1461        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1462        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1463        :return: multilines text in Markdown format as a table contains current prices.
1464        """
1465        infoText = ""
1466
1467        if show or self.pricesFile or onlyFiles:
1468            info = [
1469                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1470                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1471                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1472            ]
1473
1474            for item in iList:
1475                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1476                    item["ticker"],
1477                    item["figi"],
1478                    item["type"],
1479                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1480                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1481                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1482                    "{} / {}".format(
1483                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1484                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1485                    ),
1486                    "{} / {}".format(
1487                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1488                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1489                    ),
1490                    item["currency"],
1491                ))
1492
1493            infoText = "".join(info)
1494
1495            if show and not onlyFiles:
1496                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1497
1498            if self.pricesFile and (show or onlyFiles):
1499                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1500                    fH.write(infoText)
1501
1502                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1503
1504                if self.useHTMLReports:
1505                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1506                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1507                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1508
1509                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1510
1511        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1513    def RequestTradingStatus(self) -> dict:
1514        """
1515        Requesting trading status for the instrument defined by `figi` variable.
1516
1517        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1518
1519        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1520
1521        :return: dictionary with trading status attributes. Response example:
1522                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1523                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1524        """
1525        if self._figi is None or not self._figi:
1526            uLogger.error("Variable `figi` must be defined for using this method!")
1527            raise Exception("FIGI required")
1528
1529        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1530
1531        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1532        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1533        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1534
1535        if self.moreDebug:
1536            uLogger.debug("Records about current trading status successfully received")
1537
1538        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1540    def RequestPortfolio(self) -> dict:
1541        """
1542        Requesting actual user's portfolio for current `accountId`.
1543
1544        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1545
1546        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1547
1548        :return: dictionary with user's portfolio.
1549        """
1550        if self.accountId is None or not self.accountId:
1551            uLogger.error("Variable `accountId` must be defined for using this method!")
1552            raise Exception("Account ID required")
1553
1554        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1555
1556        self.body = str({"accountId": self.accountId})
1557        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1558        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1559
1560        if self.moreDebug:
1561            uLogger.debug("Records about user's portfolio successfully received")
1562
1563        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1565    def RequestPositions(self) -> dict:
1566        """
1567        Requesting open positions by currencies and instruments for current `accountId`.
1568
1569        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1570
1571        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1572
1573        :return: dictionary with open positions by instruments.
1574        """
1575        if self.accountId is None or not self.accountId:
1576            uLogger.error("Variable `accountId` must be defined for using this method!")
1577            raise Exception("Account ID required")
1578
1579        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1580
1581        self.body = str({"accountId": self.accountId})
1582        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1583        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1584
1585        if self.moreDebug:
1586            uLogger.debug("Records about current open positions successfully received")
1587
1588        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1590    def RequestPendingOrders(self) -> list:
1591        """
1592        Requesting current actual pending limit orders for current `accountId`.
1593
1594        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1595
1596        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1597
1598        :return: list of dictionaries with pending limit orders.
1599        """
1600        if self.accountId is None or not self.accountId:
1601            uLogger.error("Variable `accountId` must be defined for using this method!")
1602            raise Exception("Account ID required")
1603
1604        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1605
1606        self.body = str({"accountId": self.accountId})
1607        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1608        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1609
1610        if "orders" in rawResponse.keys():
1611            rawOrders = rawResponse["orders"]
1612            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1613
1614        else:
1615            rawOrders = []
1616            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1617
1618        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1620    def RequestStopOrders(self) -> list:
1621        """
1622        Requesting current actual stop orders for current `accountId`.
1623
1624        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1625
1626        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1627
1628        :return: list of dictionaries with stop orders.
1629        """
1630        if self.accountId is None or not self.accountId:
1631            uLogger.error("Variable `accountId` must be defined for using this method!")
1632            raise Exception("Account ID required")
1633
1634        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1635
1636        self.body = str({"accountId": self.accountId})
1637        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1638        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1639
1640        if "stopOrders" in rawResponse.keys():
1641            rawStopOrders = rawResponse["stopOrders"]
1642            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1643
1644        else:
1645            rawStopOrders = []
1646            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1647
1648        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full', onlyFiles=False) -> dict:
1650    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1651        """
1652        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1653        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1654        and `overviewBondsCalendarFile` are defined then also save information to file.
1655
1656        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1657        many requests about the state of the portfolio, and then, based on the received data, a large number
1658        of calculation and statistics are collected.
1659
1660        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1661        :param details: how detailed should the information be?
1662        - `full` — shows full available information about portfolio status (by default),
1663        - `positions` — shows only open positions,
1664        - `orders` — shows only sections of open limits and stop orders.
1665        - `digest` — show a short digest of the portfolio status,
1666        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1667        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1668        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1669        :return: dictionary with client's raw portfolio and some statistics.
1670        """
1671        if self.accountId is None or not self.accountId:
1672            uLogger.error("Variable `accountId` must be defined for using this method!")
1673            raise Exception("Account ID required")
1674
1675        view = {
1676            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1677                "headers": {},  # list of dictionaries, response headers without "positions" section
1678                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1679                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1680                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1681                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1682                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1683                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1684                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1685                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1686                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1687            },
1688            "stat": {  # --- some statistics calculated using "raw" sections:
1689                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1690                "availableRUB": 0.,  # available rubles (without other currencies)
1691                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1692                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1693                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1694                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1695                "sharesCostRUB": 0.,  # costs of all shares in RUB
1696                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1697                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1698                "futuresCostRUB": 0.,  # costs of all futures in RUB
1699                "Currencies": [],  # list of dictionaries of all currencies statistics
1700                "Shares": [],  # list of dictionaries of all shares statistics
1701                "Bonds": [],  # list of dictionaries of all bonds statistics
1702                "Etfs": [],  # list of dictionaries of all etfs statistics
1703                "Futures": [],  # list of dictionaries of all futures statistics
1704                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1705                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1706                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1707                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1708                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1709            },
1710            "analytics": {  # --- some analytics of portfolio:
1711                "distrByAssets": {},  # portfolio distribution by assets
1712                "distrByCompanies": {},  # portfolio distribution by companies
1713                "distrBySectors": {},  # portfolio distribution by sectors
1714                "distrByCurrencies": {},  # portfolio distribution by currencies
1715                "distrByCountries": {},  # portfolio distribution by countries
1716                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1717            }
1718        }
1719
1720        details = details.lower()
1721        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1722        if details not in availableDetails:
1723            details = "full"
1724            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1725
1726        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1727
1728        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1729        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1730        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1731        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1732
1733        # save response headers without "positions" section:
1734        for key in portfolioResponse.keys():
1735            if key != "positions":
1736                view["raw"]["headers"][key] = portfolioResponse[key]
1737
1738            else:
1739                continue
1740
1741        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1742        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1743        for item in portfolioResponse["positions"]:
1744            if item["instrumentType"] == "currency":
1745                self._figi = item["figi"]
1746                if not self._figi and item["ticker"]:
1747                    self._ticker = item["ticker"]
1748                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1749
1750                curr = self.SearchByFIGI(requestPrice=False)
1751
1752                # current price of currency in RUB:
1753                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1754                    "name": curr["name"],
1755                    "currentPrice": NanoToFloat(
1756                        item["currentPrice"]["units"],
1757                        item["currentPrice"]["nano"]
1758                    ),
1759                }
1760
1761                view["raw"]["Currencies"].append(item)
1762
1763            elif item["instrumentType"] == "share":
1764                view["raw"]["Shares"].append(item)
1765
1766            elif item["instrumentType"] == "bond":
1767                view["raw"]["Bonds"].append(item)
1768
1769            elif item["instrumentType"] == "etf":
1770                view["raw"]["Etfs"].append(item)
1771
1772            elif item["instrumentType"] == "futures":
1773                view["raw"]["Futures"].append(item)
1774
1775            else:
1776                continue
1777
1778        # how many volume of currencies (by ISO currency name) are blocked:
1779        for item in view["raw"]["positions"]["blocked"]:
1780            blocked = NanoToFloat(item["units"], item["nano"])
1781            if blocked > 0:
1782                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1783
1784        # how many volume of instruments (by FIGI) are blocked:
1785        for item in view["raw"]["positions"]["securities"]:
1786            blocked = int(item["blocked"])
1787            if blocked > 0:
1788                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1789
1790        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1791
1792        if "rub" in allBlocked.keys():
1793            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1794
1795        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1796        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1797        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1798        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1799        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1800        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1801        view["stat"]["portfolioCostRUB"] = sum([
1802            view["stat"]["allCurrenciesCostRUB"],
1803            view["stat"]["sharesCostRUB"],
1804            view["stat"]["bondsCostRUB"],
1805            view["stat"]["etfsCostRUB"],
1806            view["stat"]["futuresCostRUB"],
1807        ])
1808
1809        # --- calculating some portfolio statistics:
1810        byComp = {}  # distribution by companies
1811        bySect = {}  # distribution by sectors
1812        byCurr = {}  # distribution by currencies (include RUB)
1813        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1814        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1815
1816        for item in portfolioResponse["positions"]:
1817            self._figi = item["figi"]
1818            if not self._figi and item["ticker"]:
1819                self._ticker = item["ticker"]
1820                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1821
1822            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1823
1824            if instrument:
1825                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1826                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1827
1828                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1829                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1830
1831                else:
1832                    blocked = 0
1833
1834                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1835                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1836                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1837                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1838                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1839                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1840                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1841                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1842                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1843                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1844                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1845                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1846
1847                statData = {
1848                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1849                    "ticker": instrument["ticker"],  # ticker by FIGI
1850                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1851                    "volume": volume,  # available volume of instrument
1852                    "lots": lots,  # volume in lots of instrument
1853                    "direction": direction,  # direction of an instrument's position: short or long
1854                    "blocked": blocked,  # blocked volume of currency or instrument
1855                    "currentPrice": curPrice,  # current instrument's price in basic asset
1856                    "average": average,  # current average position price
1857                    "cost": cost,  # current cost of all volume of instrument in basic asset
1858                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1859                    "costRUB": costRUB,  # cost of instrument in ruble
1860                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1861                    "profit": profit,  # expected profit at current moment
1862                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1863                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1864                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1865                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1866                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1867                    "step": instrument["step"],  # minimum price increment
1868                }
1869
1870                # adding distribution by unique countries:
1871                if statData["country"] not in byCountry.keys():
1872                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1873
1874                else:
1875                    byCountry[statData["country"]]["cost"] += costRUB
1876                    byCountry[statData["country"]]["percent"] += percentCostRUB
1877
1878                if item["instrumentType"] != "currency":
1879                    # adding distribution by unique companies:
1880                    if statData["name"]:
1881                        if statData["name"] not in byComp.keys():
1882                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1883
1884                        else:
1885                            byComp[statData["name"]]["cost"] += costRUB
1886                            byComp[statData["name"]]["percent"] += percentCostRUB
1887
1888                    # adding distribution by unique sectors:
1889                    if statData["sector"] not in bySect.keys():
1890                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1891
1892                    else:
1893                        bySect[statData["sector"]]["cost"] += costRUB
1894                        bySect[statData["sector"]]["percent"] += percentCostRUB
1895
1896                # adding distribution by unique currencies:
1897                if currency not in byCurr.keys():
1898                    byCurr[currency] = {
1899                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1900                        "cost": costRUB,
1901                        "percent": percentCostRUB
1902                    }
1903
1904                else:
1905                    byCurr[currency]["cost"] += costRUB
1906                    byCurr[currency]["percent"] += percentCostRUB
1907
1908                # saving statistics for every instrument:
1909                if item["instrumentType"] == "currency":
1910                    view["stat"]["Currencies"].append(statData)
1911
1912                    # update dict with free funds for trading (total - blocked) by currencies
1913                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1914                    view["stat"]["funds"][currency] = {
1915                        "total": volume,
1916                        "totalCostRUB": costRUB,  # total volume cost in rubles
1917                        "free": volume - blocked,
1918                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1919                    }
1920
1921                elif item["instrumentType"] == "share":
1922                    view["stat"]["Shares"].append(statData)
1923
1924                elif item["instrumentType"] == "bond":
1925                    view["stat"]["Bonds"].append(statData)
1926
1927                elif item["instrumentType"] == "etf":
1928                    view["stat"]["Etfs"].append(statData)
1929
1930                elif item["instrumentType"] == "Futures":
1931                    view["stat"]["Futures"].append(statData)
1932
1933                else:
1934                    continue
1935
1936        # total changes in Russian Ruble:
1937        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1938        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1939        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1940        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1941        view["stat"]["funds"]["rub"] = {
1942            "total": view["stat"]["availableRUB"],
1943            "totalCostRUB": view["stat"]["availableRUB"],
1944            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1945            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1946        }
1947
1948        # --- pending limit orders sector data:
1949        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1950        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1951
1952        for item in view["raw"]["orders"]:
1953            self._figi = item["figi"]
1954
1955            if item["figi"] not in uniquePendingOrdersFIGIs:
1956                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1957
1958                uniquePendingOrdersFIGIs.append(item["figi"])
1959                uniquePendingOrders[item["figi"]] = instrument
1960
1961            else:
1962                instrument = uniquePendingOrders[item["figi"]]
1963
1964            if instrument:
1965                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1966                orderType = TKS_ORDER_TYPES[item["orderType"]]
1967                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1968                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1969
1970                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1971                if item["direction"] == "ORDER_DIRECTION_BUY":
1972                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1973
1974                else:
1975                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1976
1977                # requested price for order execution:
1978                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1979
1980                # necessary changes in percent to reach target from current price:
1981                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1982
1983                view["stat"]["orders"].append({
1984                    "orderID": item["orderId"],  # orderId number parameter of current order
1985                    "figi": item["figi"],  # FIGI identification
1986                    "ticker": instrument["ticker"],  # ticker name by FIGI
1987                    "lotsRequested": item["lotsRequested"],  # requested lots value
1988                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1989                    "currentPrice": lastPrice,  # current instrument's price for defined action
1990                    "targetPrice": target,  # requested price for order execution in base currency
1991                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1992                    "percentChanges": changes,  # changes in percent to target from current price
1993                    "currency": item["currency"],  # instrument's currency name
1994                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1995                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1996                    "status": orderState,  # order status from TKS_ORDER_STATES
1997                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1998                })
1999
2000        # --- stop orders sector data:
2001        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2002        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2003
2004        for item in view["raw"]["stopOrders"]:
2005            self._figi = item["figi"]
2006
2007            if item["figi"] not in uniqueStopOrdersFIGIs:
2008                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2009
2010                uniqueStopOrdersFIGIs.append(item["figi"])
2011                uniqueStopOrders[item["figi"]] = instrument
2012
2013            else:
2014                instrument = uniqueStopOrders[item["figi"]]
2015
2016            if instrument:
2017                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2018                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2019                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2020
2021                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2022                if "expirationTime" in item.keys():
2023                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2024                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2025
2026                else:
2027                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2028                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2029
2030                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2031                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2032                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2033
2034                else:
2035                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2036
2037                # requested price when stop-order executed:
2038                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2039
2040                # price for limit-order, set up when stop-order executed:
2041                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2042
2043                # necessary changes in percent to reach target from current price:
2044                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2045
2046                view["stat"]["stopOrders"].append({
2047                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2048                    "figi": item["figi"],  # FIGI identification
2049                    "ticker": instrument["ticker"],  # ticker name by FIGI
2050                    "lotsRequested": item["lotsRequested"],  # requested lots value
2051                    "currentPrice": lastPrice,  # current instrument's price for defined action
2052                    "targetPrice": target,  # requested price for stop-order execution in base currency
2053                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2054                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2055                    "percentChanges": changes,  # changes in percent to target from current price
2056                    "currency": item["currency"],  # instrument's currency name
2057                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2058                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2059                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2060                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2061                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2062                })
2063
2064        # --- calculating data for analytics section:
2065        # portfolio distribution by assets:
2066        view["analytics"]["distrByAssets"] = {
2067            "Ruble": {
2068                "uniques": 1,
2069                "cost": view["stat"]["availableRUB"],
2070                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2071            },
2072            "Currencies": {
2073                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2074                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2075                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2076            },
2077            "Shares": {
2078                "uniques": len(view["stat"]["Shares"]),
2079                "cost": view["stat"]["sharesCostRUB"],
2080                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2081            },
2082            "Bonds": {
2083                "uniques": len(view["stat"]["Bonds"]),
2084                "cost": view["stat"]["bondsCostRUB"],
2085                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2086            },
2087            "Etfs": {
2088                "uniques": len(view["stat"]["Etfs"]),
2089                "cost": view["stat"]["etfsCostRUB"],
2090                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2091            },
2092            "Futures": {
2093                "uniques": len(view["stat"]["Futures"]),
2094                "cost": view["stat"]["futuresCostRUB"],
2095                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2096            },
2097        }
2098
2099        # portfolio distribution by companies:
2100        view["analytics"]["distrByCompanies"]["All money cash"] = {
2101            "ticker": "",
2102            "cost": view["stat"]["allCurrenciesCostRUB"],
2103            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2104        }
2105        view["analytics"]["distrByCompanies"].update(byComp)
2106
2107        # portfolio distribution by sectors:
2108        view["analytics"]["distrBySectors"]["All money cash"] = {
2109            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2110            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2111        }
2112        view["analytics"]["distrBySectors"].update(bySect)
2113
2114        # portfolio distribution by currencies:
2115        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2116            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2117
2118            if self.moreDebug:
2119                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2120
2121        view["analytics"]["distrByCurrencies"].update(byCurr)
2122        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2123        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2124
2125        # portfolio distribution by countries:
2126        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2127            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2128
2129            if self.moreDebug:
2130                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2131
2132        view["analytics"]["distrByCountries"].update(byCountry)
2133        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2134        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2135
2136        # --- Prepare text statistics overview in human-readable:
2137        if show or onlyFiles:
2138            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2139
2140            # Whatever the value `details`, header not changes:
2141            info = [
2142                "# Client's portfolio\n\n",
2143                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2144                "* **Account ID:** [{}]\n".format(self.accountId),
2145            ]
2146
2147            if details in ["full", "positions", "digest"]:
2148                info.extend([
2149                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2150                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2151                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2152                        view["stat"]["totalChangesRUB"],
2153                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2154                        view["stat"]["totalChangesPercentRUB"],
2155                    ),
2156                ])
2157
2158            if details in ["full", "positions"]:
2159                info.extend([
2160                    "## Open positions\n\n",
2161                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2162                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2163                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2164                        "{:.2f} ({:.2f}) rub".format(
2165                            view["stat"]["availableRUB"],
2166                            view["stat"]["blockedRUB"],
2167                        )
2168                    )
2169                ])
2170
2171                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2172                    return [
2173                        "|                             |                                 |          |              |              |                     |                              |\n",
2174                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2175                            noTradeStr if noTradeStr else typeStr,
2176                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2177                        ),
2178                    ]
2179
2180                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2181                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2182                        "{} [{}]".format(data["ticker"], data["figi"]),
2183                        "{:.2f} ({:.2f}) {}".format(
2184                            data["volume"],
2185                            data["blocked"],
2186                            data["currency"],
2187                        ) if isCurr else "{:.0f} ({:.0f})".format(
2188                            data["volume"],
2189                            data["blocked"],
2190                        ),
2191                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2192                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2193                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2194                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2195                        "{}{:.2f} {} ({}{:.2f}%)".format(
2196                            "+" if data["profit"] > 0 else "",
2197                            data["profit"], data["baseCurrencyName"],
2198                            "+" if data["percentProfit"] > 0 else "",
2199                            data["percentProfit"],
2200                        ),
2201                    )
2202
2203                # --- Show currencies section:
2204                if view["stat"]["Currencies"]:
2205                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2206                    for item in view["stat"]["Currencies"]:
2207                        info.append(_InfoStr(item, isCurr=True))
2208
2209                else:
2210                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2211
2212                # --- Show shares section:
2213                if view["stat"]["Shares"]:
2214                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2215
2216                    for item in view["stat"]["Shares"]:
2217                        info.append(_InfoStr(item))
2218
2219                else:
2220                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2221
2222                # --- Show bonds section:
2223                if view["stat"]["Bonds"]:
2224                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2225
2226                    for item in view["stat"]["Bonds"]:
2227                        info.append(_InfoStr(item))
2228
2229                else:
2230                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2231
2232                # --- Show etfs section:
2233                if view["stat"]["Etfs"]:
2234                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2235
2236                    for item in view["stat"]["Etfs"]:
2237                        info.append(_InfoStr(item))
2238
2239                else:
2240                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2241
2242                # --- Show futures section:
2243                if view["stat"]["Futures"]:
2244                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2245
2246                    for item in view["stat"]["Futures"]:
2247                        info.append(_InfoStr(item))
2248
2249                else:
2250                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2251
2252            if details in ["full", "orders"]:
2253                # --- Show pending limit orders section:
2254                if view["stat"]["orders"]:
2255                    info.extend([
2256                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2257                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2258                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2259                    ])
2260
2261                    for item in view["stat"]["orders"]:
2262                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2263                            "{} [{}]".format(item["ticker"], item["figi"]),
2264                            item["orderID"],
2265                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2266                            "{} {} ({}{:.2f}%)".format(
2267                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2268                                item["baseCurrencyName"],
2269                                "+" if item["percentChanges"] > 0 else "",
2270                                float(item["percentChanges"]),
2271                            ),
2272                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2273                            item["action"],
2274                            item["type"],
2275                            item["date"],
2276                        ))
2277
2278                else:
2279                    info.append("\n## Total pending limit-orders: [0]\n")
2280
2281                # --- Show stop orders section:
2282                if view["stat"]["stopOrders"]:
2283                    info.extend([
2284                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2285                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2286                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2287                    ])
2288
2289                    for item in view["stat"]["stopOrders"]:
2290                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2291                            "{} [{}]".format(item["ticker"], item["figi"]),
2292                            item["orderID"],
2293                            item["lotsRequested"],
2294                            "{} {} ({}{:.2f}%)".format(
2295                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2296                                item["baseCurrencyName"],
2297                                "+" if item["percentChanges"] > 0 else "",
2298                                float(item["percentChanges"]),
2299                            ),
2300                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2301                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2302                            item["action"],
2303                            item["type"],
2304                            item["expType"],
2305                            item["createDate"],
2306                            item["expDate"],
2307                        ))
2308
2309                else:
2310                    info.append("\n## Total stop-orders: [0]\n")
2311
2312            if details in ["full", "analytics"]:
2313                # -- Show analytics section:
2314                if view["stat"]["portfolioCostRUB"] > 0:
2315                    info.extend([
2316                        "\n# Analytics\n\n"
2317                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2318                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2319                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2320                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2321                            view["stat"]["totalChangesRUB"],
2322                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2323                            view["stat"]["totalChangesPercentRUB"],
2324                        ),
2325                        "\n## Portfolio distribution by assets\n"
2326                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2327                        "|------------------------------------|---------|---------|--------------------|\n",
2328                    ])
2329
2330                    for key in view["analytics"]["distrByAssets"].keys():
2331                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2332                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2333                                key,
2334                                view["analytics"]["distrByAssets"][key]["uniques"],
2335                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2336                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2337                            ))
2338
2339                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2340
2341                    info.extend([
2342                        "\n## Portfolio distribution by companies\n"
2343                        "\n| Company                                      | Percent | Current cost       |\n",
2344                        aSepLine,
2345                    ])
2346
2347                    for company in view["analytics"]["distrByCompanies"].keys():
2348                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2349                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2350                                "{}{}".format(
2351                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2352                                    company,
2353                                ),
2354                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2355                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2356                            ))
2357
2358                    info.extend([
2359                        "\n## Portfolio distribution by sectors\n"
2360                        "\n| Sector                                       | Percent | Current cost       |\n",
2361                        aSepLine,
2362                    ])
2363
2364                    for sector in view["analytics"]["distrBySectors"].keys():
2365                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2366                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2367                                sector,
2368                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2369                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2370                            ))
2371
2372                    info.extend([
2373                        "\n## Portfolio distribution by currencies\n"
2374                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2375                        aSepLine,
2376                    ])
2377
2378                    for curr in view["analytics"]["distrByCurrencies"].keys():
2379                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2380                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2381                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2382                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2383                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2384                            ))
2385
2386                    info.extend([
2387                        "\n## Portfolio distribution by countries\n"
2388                        "\n| Assets by country                            | Percent | Current cost       |\n",
2389                        aSepLine,
2390                    ])
2391
2392                    for country in view["analytics"]["distrByCountries"].keys():
2393                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2394                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2395                                country,
2396                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2397                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2398                            ))
2399
2400            if details in ["full", "calendar"]:
2401                # -- Show bonds payment calendar section:
2402                if view["stat"]["Bonds"]:
2403                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2404                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2405                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2406
2407                else:
2408                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2409
2410            infoText = "".join(info)
2411
2412            if show and not onlyFiles:
2413                uLogger.info(infoText)
2414
2415            if details == "full" and self.overviewFile:
2416                filename = self.overviewFile
2417
2418            elif details == "digest" and self.overviewDigestFile:
2419                filename = self.overviewDigestFile
2420
2421            elif details == "positions" and self.overviewPositionsFile:
2422                filename = self.overviewPositionsFile
2423
2424            elif details == "orders" and self.overviewOrdersFile:
2425                filename = self.overviewOrdersFile
2426
2427            elif details == "analytics" and self.overviewAnalyticsFile:
2428                filename = self.overviewAnalyticsFile
2429
2430            elif details == "calendar" and self.overviewBondsCalendarFile:
2431                filename = self.overviewBondsCalendarFile
2432
2433            else:
2434                filename = ""
2435
2436            if filename and (show or onlyFiles):
2437                with open(filename, "w", encoding="UTF-8") as fH:
2438                    fH.write(infoText)
2439
2440                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2441
2442                if self.useHTMLReports:
2443                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2444                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2445                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2446
2447                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2448
2449        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio).
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2451    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2452        """
2453        Returns history operations between two given dates for current `accountId`.
2454        If `reportFile` string is not empty then also save human-readable report.
2455        Shows some statistical data of closed positions.
2456
2457        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2458        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2459        :param show: if `True` then also prints all records to the console.
2460        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2461        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2462        :return: original list of dictionaries with history of deals records from API ("operations" key):
2463                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2464                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2465        """
2466        if self.accountId is None or not self.accountId:
2467            uLogger.error("Variable `accountId` must be defined for using this method!")
2468            raise Exception("Account ID required")
2469
2470        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2471
2472        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2473
2474        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2475        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2476        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2477        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2478        customStat = {}  # custom statistics in additional to responseJSON
2479
2480        # --- output report in human-readable format:
2481        if self.reportFile and (show or onlyFiles):
2482            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2483            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2484            nextDay = ""
2485
2486            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2487
2488            if len(ops) > 0:
2489                customStat = {
2490                    "opsCount": 0,  # total operations count
2491                    "buyCount": 0,  # buy operations
2492                    "sellCount": 0,  # sell operations
2493                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2494                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2495                    "payIn": {"rub": 0.},  # Deposit brokerage account
2496                    "payOut": {"rub": 0.},  # Withdrawals
2497                    "divs": {"rub": 0.},  # Dividends income
2498                    "coupons": {"rub": 0.},  # Coupon's income
2499                    "brokerCom": {"rub": 0.},  # Service commissions
2500                    "serviceCom": {"rub": 0.},  # Service commissions
2501                    "marginCom": {"rub": 0.},  # Margin commissions
2502                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2503                }
2504
2505                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2506                for item in ops:
2507                    if item["state"] == "OPERATION_STATE_EXECUTED":
2508                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2509
2510                        # count buy operations:
2511                        if "_BUY" in item["operationType"]:
2512                            customStat["buyCount"] += 1
2513
2514                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2515                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2516
2517                            else:
2518                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2519
2520                        # count sell operations:
2521                        elif "_SELL" in item["operationType"]:
2522                            customStat["sellCount"] += 1
2523
2524                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2525                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2529
2530                        # count incoming operations:
2531                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2532                            if item["payment"]["currency"] in customStat["payIn"].keys():
2533                                customStat["payIn"][item["payment"]["currency"]] += payment
2534
2535                            else:
2536                                customStat["payIn"][item["payment"]["currency"]] = payment
2537
2538                        # count withdrawals operations:
2539                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2540                            if item["payment"]["currency"] in customStat["payOut"].keys():
2541                                customStat["payOut"][item["payment"]["currency"]] += payment
2542
2543                            else:
2544                                customStat["payOut"][item["payment"]["currency"]] = payment
2545
2546                        # count dividends income:
2547                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2548                            if item["payment"]["currency"] in customStat["divs"].keys():
2549                                customStat["divs"][item["payment"]["currency"]] += payment
2550
2551                            else:
2552                                customStat["divs"][item["payment"]["currency"]] = payment
2553
2554                        # count coupon's income:
2555                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2556                            if item["payment"]["currency"] in customStat["coupons"].keys():
2557                                customStat["coupons"][item["payment"]["currency"]] += payment
2558
2559                            else:
2560                                customStat["coupons"][item["payment"]["currency"]] = payment
2561
2562                        # count broker commissions:
2563                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2564                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2565                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2566
2567                            else:
2568                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2569
2570                        # count service commissions:
2571                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2572                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2573                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2574
2575                            else:
2576                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2577
2578                        # count margin commissions:
2579                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2580                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2581                                customStat["marginCom"][item["payment"]["currency"]] += payment
2582
2583                            else:
2584                                customStat["marginCom"][item["payment"]["currency"]] = payment
2585
2586                        # count withholding taxes:
2587                        elif "_TAX" in item["operationType"]:
2588                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2589                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2590
2591                            else:
2592                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2593
2594                        else:
2595                            continue
2596
2597                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2598
2599                # --- view "Actions" lines:
2600                info.extend([
2601                    "| Report sections            |                               |                              |                      |                        |\n",
2602                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2603                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2604                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2605                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2606                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2607                    ),
2608                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2609                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2610                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2611                    ),
2612                ])
2613
2614                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2615                for key in opsKeys:
2616                    if key == "rub":
2617                        continue
2618
2619                    info.extend([
2620                        "|                            |                               | {:<28} |                      |                        |\n".format(
2621                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2622                        ),
2623                        "|                            |                               | {:<28} |                      |                        |\n".format(
2624                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2625                        ),
2626                    ])
2627
2628                info.append(splitLine1)
2629
2630                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2631                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2632                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2633                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2634                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2635                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2636                    )
2637
2638                # --- view "Payments" lines:
2639                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2640                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2641
2642                for key in paymentsKeys:
2643                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2644
2645                info.append(splitLine1)
2646
2647                # --- view "Commissions and taxes" lines:
2648                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2649                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2650
2651                for key in comKeys:
2652                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2653
2654                info.extend([
2655                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2656                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2657                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2658                ])
2659
2660            else:
2661                info.append("Broker returned no operations during this period\n")
2662
2663            # --- view "Operations" section:
2664            for item in ops:
2665                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2666                    continue
2667
2668                else:
2669                    self._figi = item["figi"]
2670                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2671                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2672
2673                    # group of deals during one day:
2674                    if nextDay and item["date"].split("T")[0] != nextDay:
2675                        info.append(splitLine2)
2676                        nextDay = ""
2677
2678                    else:
2679                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2680
2681                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2682                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2683                        self._figi if self._figi else "—",
2684                        instrument["ticker"] if instrument else "—",
2685                        instrument["type"] if instrument else "—",
2686                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2687                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2688                        TKS_OPERATION_STATES[item["state"]],
2689                        TKS_OPERATION_TYPES[item["operationType"]],
2690                    ))
2691
2692            infoText = "".join(info)
2693
2694            if show and not onlyFiles:
2695                if self.moreDebug:
2696                    uLogger.debug("Records about history of a client's operations successfully received")
2697
2698                uLogger.info(infoText)
2699
2700            if self.reportFile and (show or onlyFiles):
2701                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2702                    fH.write(infoText)
2703
2704                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2705
2706                if self.useHTMLReports:
2707                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2708                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2709                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2710
2711                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2712
2713        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False, onlyFiles=False) -> pandas.core.frame.DataFrame:
2715    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2716        """
2717        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2718
2719        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2720        Warning! Broker server used ISO UTC time by default.
2721
2722        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2723        Also, `historyFile` used to update history with `onlyMissing` parameter.
2724
2725        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2726
2727        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2728        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2729        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2730                         `"hour"`, `"day"`. Default: `"hour"`.
2731        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2732                            False by default. Warning! History appends only from last candle to current time
2733                            with always update last candle!
2734        :param csvSep: separator if csv-file is used, `,` by default.
2735        :param show: if `True` then also prints Pandas DataFrame to the console.
2736        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2737        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2738                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2739        """
2740        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2741        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2742        history = None  # empty pandas object for history
2743
2744        if interval not in TKS_CANDLE_INTERVALS.keys():
2745            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2746            raise Exception("Incorrect value")
2747
2748        if not (self._ticker or self._figi):
2749            uLogger.error("Ticker or FIGI must be defined!")
2750            raise Exception("Ticker or FIGI required")
2751
2752        if self._ticker and not self._figi:
2753            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2754            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2755
2756        if self._figi and not self._ticker:
2757            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2758            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2759
2760        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2761        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2762        if interval.lower() != "day":
2763            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2764
2765        delta = dtEnd - dtStart  # current UTC time minus last time in file
2766        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2767
2768        # calculate history length in candles:
2769        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2770        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2771            length += 1  # to avoid fraction time
2772
2773        # calculate data blocks count:
2774        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2775
2776        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2777        if self.moreDebug:
2778            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2779            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2780            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2781            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2782
2783        tempOld = None  # pandas object for old history, if --only-missing key present
2784        lastTime = None  # datetime object of last old candle in file
2785
2786        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2787            if self.moreDebug:
2788                uLogger.debug("--only-missing key present, add only last missing candles...")
2789                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2790
2791            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2792
2793            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2794            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2795            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2796            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2797
2798            # get last datetime object from last string in file or minus 1 delta if file is empty:
2799            if len(tempOld) > 0:
2800                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2801
2802            else:
2803                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2804
2805            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2806
2807        responseJSONs = []  # raw history blocks of data
2808
2809        blockEnd = dtEnd
2810        for item in range(blocks):
2811            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2812            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2813
2814            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2815                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2816            ))
2817
2818            if blockStart == blockEnd:
2819                uLogger.debug("Skipped this zero-length block...")
2820
2821            else:
2822                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2823                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2824                self.body = str({
2825                    "figi": self._figi,
2826                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2827                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2828                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2829                })
2830                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2831
2832                if "code" in responseJSON.keys():
2833                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2834
2835                else:
2836                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2837                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2838
2839                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2840
2841            blockEnd = blockStart
2842
2843        printCount = len(responseJSONs)  # candles to show in console
2844        if responseJSONs:
2845            tempHistory = pd.DataFrame(
2846                data={
2847                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2848                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2849                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2850                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2851                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2852                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2853                    "volume": [int(item["volume"]) for item in responseJSONs],
2854                },
2855                index=range(len(responseJSONs)),
2856                columns=["date", "time", "open", "high", "low", "close", "volume"],
2857            )
2858            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2859            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2860
2861            # append only newest candles to old history if --only-missing key present:
2862            if onlyMissing and tempOld is not None and lastTime is not None:
2863                index = 0  # find start index in tempHistory data:
2864
2865                for i, item in tempHistory.iterrows():
2866                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2867
2868                    if curTime == lastTime:
2869                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2870                        index = i
2871                        printCount = i + 1
2872                        break
2873
2874                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2875
2876            else:
2877                history = tempHistory  # if no `--only-missing` key then load full data from server
2878
2879            if self.moreDebug:
2880                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2881
2882        if history is not None and not history.empty:
2883            if show and not onlyFiles:
2884                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2885                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2886                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2887                ))
2888
2889        else:
2890            uLogger.warning("Received an empty candles history!")
2891
2892        if self.historyFile is not None:
2893            if history is not None and not history.empty:
2894                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2895                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2896
2897            else:
2898                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2899
2900        else:
2901            if self.moreDebug:
2902                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2903
2904        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2906    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2907        """
2908        Load candles history from csv-file and return Pandas DataFrame object.
2909
2910        See also: `History()` and `ShowHistoryChart()` methods.
2911
2912        :param filePath: path to csv-file to open.
2913        """
2914        loadedHistory = None  # init candles data object
2915
2916        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2917
2918        if os.path.exists(filePath):
2919            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2920
2921            tfStr = self.priceModel.FormattedDelta(
2922                self.priceModel.timeframe,
2923                "{days} days {hours}h {minutes}m {seconds}s",
2924            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2925                self.priceModel.timeframe,
2926                "{hours}h {minutes}m {seconds}s",
2927            )
2928
2929            if loadedHistory is not None and not loadedHistory.empty:
2930                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2931                    len(loadedHistory),
2932                    tfStr,
2933                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2934                )
2935
2936            else:
2937                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2938
2939        else:
2940            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2941
2942        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2944    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2945        """
2946        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2947
2948        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2949        Default: `index.html` (both for interact and non-interact candlesticks chart).
2950
2951        See also: `History()` and `LoadHistory()` methods.
2952
2953        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2954        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2955                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2956                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2957                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2958        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2959                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2960        """
2961        if isinstance(candles, str):
2962            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2963            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2964
2965        elif isinstance(candles, pd.DataFrame):
2966            self.priceModel.prices = candles  # set candles chain from variable
2967            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2968
2969            if "datetime" not in candles.columns:
2970                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2971
2972        else:
2973            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2974            raise Exception("Incorrect value")
2975
2976        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2977
2978        if interact:
2979            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2980
2981            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2982
2983        else:
2984            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2985
2986            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2987
2988        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2990    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2991        """
2992        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2993        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2994
2995        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2996
2997        :param operation: string "Buy" or "Sell".
2998        :param lots: volume, integer count of lots >= 1.
2999        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
3000        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3001        :param expDate: string "Undefined" by default or local date in future,
3002                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3003        :return: JSON with response from broker server.
3004        """
3005        if self.accountId is None or not self.accountId:
3006            uLogger.error("Variable `accountId` must be defined for using this method!")
3007            raise Exception("Account ID required")
3008
3009        if operation is None or not operation or operation not in ("Buy", "Sell"):
3010            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3011            raise Exception("Incorrect value")
3012
3013        if lots is None or lots < 1:
3014            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3015            lots = 1
3016
3017        if tp is None or tp < 0:
3018            tp = 0
3019
3020        if sl is None or sl < 0:
3021            sl = 0
3022
3023        if expDate is None or not expDate:
3024            expDate = "Undefined"
3025
3026        if not (self._ticker or self._figi):
3027            uLogger.error("Ticker or FIGI must be defined!")
3028            raise Exception("Ticker or FIGI required")
3029
3030        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3031        self._ticker = instrument["ticker"]
3032        self._figi = instrument["figi"]
3033
3034        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3035
3036        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3037        self.body = str({
3038            "figi": self._figi,
3039            "quantity": str(lots),
3040            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3041            "accountId": str(self.accountId),
3042            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3043        })
3044        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3045
3046        if "orderId" in response.keys():
3047            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3048                operation, response["orderId"],
3049                self._ticker, self._figi, lots,
3050                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3051                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3052                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3053            ))
3054
3055            if tp > 0:
3056                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3057
3058            if sl > 0:
3059                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3060
3061        else:
3062            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3063
3064        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3066    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3067        """
3068        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3069        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3070
3071        See also: `Order()` and `Trade()` docstrings.
3072
3073        :param lots: volume, integer count of lots >= 1.
3074        :param tp: float > 0, take profit price of stop-order.
3075        :param sl: float > 0, stop loss price of stop-order.
3076        :param expDate: it's a local date in future.
3077                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3078        :return: JSON with response from broker server.
3079        """
3080        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3082    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3083        """
3084        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3085        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3086
3087        See also: `Order()` and `Trade()` docstrings.
3088
3089        :param lots: volume, integer count of lots >= 1.
3090        :param tp: float > 0, take profit price of stop-order.
3091        :param sl: float > 0, stop loss price of stop-order.
3092        :param expDate: it's a local date in the future.
3093                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3094        :return: JSON with response from broker server.
3095        """
3096        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3098    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3099        """
3100        Close position of given instruments.
3101
3102        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3103        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3104                         This avoids unnecessary downloading data from the server.
3105        """
3106        if instruments is None or not instruments:
3107            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3108            raise Exception("Ticker or FIGI required")
3109
3110        if isinstance(instruments, str):
3111            instruments = [instruments]
3112
3113        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3114        if uniqueInstruments:
3115            if portfolio is None or not portfolio:
3116                portfolio = self.Overview(show=False)
3117
3118            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3119            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3120
3121            for self._figi in uniqueInstruments:
3122                if self._figi not in allOpened:
3123                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3124                    continue
3125
3126                # search open trade info about instrument by ticker:
3127                instrument = {}
3128                for iType in TKS_INSTRUMENTS:
3129                    if instrument:
3130                        break
3131
3132                    for item in portfolio["stat"][iType]:
3133                        if item["figi"] == self._figi:
3134                            instrument = item
3135                            break
3136
3137                if instrument:
3138                    self._ticker = instrument["ticker"]
3139                    self._figi = instrument["figi"]
3140
3141                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3142                        self._ticker,
3143                        self._figi,
3144                        int(instrument["volume"]),
3145                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3146                    ))
3147
3148                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3149
3150                    if tradeLots > 0:
3151                        if instrument["blocked"] > 0:
3152                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3153                                instrument["blocked"],
3154                                self._ticker,
3155                                tradeLots,
3156                            ))
3157
3158                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3159                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3160
3161                    else:
3162                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3164    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3165        """
3166        Close all positions of given instruments with defined type.
3167
3168        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3169        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3170                         This avoids unnecessary downloading data from the server.
3171        """
3172        if iType not in TKS_INSTRUMENTS:
3173            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3174
3175        else:
3176            if portfolio is None or not portfolio:
3177                portfolio = self.Overview(show=False)
3178
3179            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3180            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3181
3182            if tickers and portfolio:
3183                self.CloseTrades(tickers, portfolio)
3184
3185            else:
3186                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3188    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3189        """
3190        Universal method to create market or limit orders with all available parameters for current `accountId`.
3191        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3192
3193        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3194        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3195
3196        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3197        then broker immediately open market order as you can do simple --buy or --sell operations!
3198
3199        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3200        When current price will go up or down to target price value then broker opens a limit order.
3201        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3202
3203        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3204
3205        :param operation: string "Buy" or "Sell".
3206        :param orderType: string "Limit" or "Stop".
3207        :param lots: volume, integer count of lots >= 1.
3208        :param targetPrice: target price > 0. This is open trade price for limit order.
3209        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3210                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3211        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3212                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3213                         Stop loss order always executed by market price.
3214        :param expDate: string "Undefined" by default or local date in future.
3215                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3216                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3217                        A limit order has no expiration date, it lasts until the end of the trading day.
3218        :return: JSON with response from broker server.
3219        """
3220        if self.accountId is None or not self.accountId:
3221            uLogger.error("Variable `accountId` must be defined for using this method!")
3222            raise Exception("Account ID required")
3223
3224        if operation is None or not operation or operation not in ("Buy", "Sell"):
3225            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3226            raise Exception("Incorrect value")
3227
3228        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3229            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3230            raise Exception("Incorrect value")
3231
3232        if lots is None or lots < 1:
3233            uLogger.error("You must define trade volume > 0: integer count of lots!")
3234            raise Exception("Incorrect value")
3235
3236        if targetPrice is None or targetPrice <= 0:
3237            uLogger.error("Target price for limit-order must be greater than 0!")
3238            raise Exception("Incorrect value")
3239
3240        if limitPrice is None or limitPrice <= 0:
3241            limitPrice = targetPrice
3242
3243        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3244            stopType = "Limit"
3245
3246        if expDate is None or not expDate:
3247            expDate = "Undefined"
3248
3249        if not (self._ticker or self._figi):
3250            uLogger.error("Tocker or FIGI must be defined!")
3251            raise Exception("Ticker or FIGI required")
3252
3253        response = {}
3254        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3255        self._ticker = instrument["ticker"]
3256        self._figi = instrument["figi"]
3257
3258        if orderType == "Limit":
3259            uLogger.debug(
3260                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3261                    self._ticker, self._figi,
3262                    operation, lots, targetPrice, instrument["currency"],
3263                ))
3264
3265            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3266            self.body = str({
3267                "figi": self._figi,
3268                "quantity": str(lots),
3269                "price": FloatToNano(targetPrice),
3270                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3271                "accountId": str(self.accountId),
3272                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3273            })
3274            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3275
3276            if "orderId" in response.keys():
3277                uLogger.info(
3278                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3279                        response["orderId"], self._ticker, self._figi, operation, lots,
3280                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3281                    ))
3282
3283                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3284                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3285                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3286                            targetPrice, instrument["currency"],
3287                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3288                        ))
3289
3290                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3291                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3292                            targetPrice, instrument["currency"],
3293                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3294                        ))
3295
3296            else:
3297                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3298
3299        if orderType == "Stop":
3300            uLogger.debug(
3301                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3302                    self._ticker, self._figi,
3303                    operation, lots,
3304                    targetPrice, instrument["currency"],
3305                    limitPrice, instrument["currency"],
3306                    stopType, expDate,
3307                ))
3308
3309            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3310            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3311            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3312
3313            body = {
3314                "figi": self._figi,
3315                "quantity": str(lots),
3316                "price": FloatToNano(limitPrice),
3317                "stopPrice": FloatToNano(targetPrice),
3318                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3319                "accountId": str(self.accountId),
3320                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3321                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3322            }
3323
3324            if expDateUTC:
3325                body["expireDate"] = expDateUTC
3326
3327            self.body = str(body)
3328            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3329
3330            if "stopOrderId" in response.keys():
3331                uLogger.info(
3332                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3333                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3334                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3335                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3336                        TKS_STOP_ORDER_TYPES[stopOrderType],
3337                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3338                    ))
3339
3340                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3341                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3342                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3343                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3344                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3345                        ))
3346
3347                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3348                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3349                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3350                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3351                        ))
3352
3353            else:
3354                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3355
3356        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3358    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3359        """
3360        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3361        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3362        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3363        See also: `Order()` docstring.
3364
3365        :param lots: volume, integer count of lots >= 1.
3366        :param targetPrice: target price > 0. This is open trade price for limit order.
3367        :return: JSON with response from broker server.
3368        """
3369        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3371    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3372        """
3373        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3374        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3375        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3376        target price value then broker opens a limit order. See also: `Order()` docstring.
3377
3378        :param lots: volume, integer count of lots >= 1.
3379        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3380        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3381                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3382        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3383                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3384        :param expDate: string "Undefined" by default or local date in future.
3385                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3386                        This date is converting to UTC format for server.
3387        :return: JSON with response from broker server.
3388        """
3389        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3391    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3392        """
3393        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3394        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3395        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3396        See also: `Order()` docstring.
3397
3398        :param lots: volume, integer count of lots >= 1.
3399        :param targetPrice: target price > 0. This is open trade price for limit order.
3400        :return: JSON with response from broker server.
3401        """
3402        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3404    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3405        """
3406        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3407        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3408        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3409        target price value then broker opens a limit order. See also: `Order()` docstring.
3410
3411        :param lots: volume, integer count of lots >= 1.
3412        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3413        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3414                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3415        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3416                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3417        :param expDate: string "Undefined" by default or local date in future.
3418                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3419                        This date is converting to UTC format for server.
3420        :return: JSON with response from broker server.
3421        """
3422        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3424    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3425        """
3426        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3427
3428        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3429        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3430                             This avoids unnecessary downloading data from the server.
3431        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3432        """
3433        if self.accountId is None or not self.accountId:
3434            uLogger.error("Variable `accountId` must be defined for using this method!")
3435            raise Exception("Account ID required")
3436
3437        if orderIDs:
3438            if allOrdersIDs is None:
3439                rawOrders = self.RequestPendingOrders()
3440                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3441
3442            if allStopOrdersIDs is None:
3443                rawStopOrders = self.RequestStopOrders()
3444                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3445
3446            for orderID in orderIDs:
3447                idInPendingOrders = orderID in allOrdersIDs
3448                idInStopOrders = orderID in allStopOrdersIDs
3449
3450                if not (idInPendingOrders or idInStopOrders):
3451                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3452                    continue
3453
3454                else:
3455                    if idInPendingOrders:
3456                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3457
3458                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3459                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3460                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3461                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3462
3463                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3464                            if self.moreDebug:
3465                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3466
3467                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3468
3469                        else:
3470                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3471
3472                    elif idInStopOrders:
3473                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3474
3475                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3476                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3477                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3478                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3479
3480                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3481                            if self.moreDebug:
3482                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3483
3484                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3485
3486                        else:
3487                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3488
3489                    else:
3490                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3492    def CloseAllOrders(self) -> None:
3493        """
3494        Gets a list of open pending and stop orders and cancel it all.
3495        """
3496        rawOrders = self.RequestPendingOrders()
3497        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3498        lenOrders = len(allOrdersIDs)
3499
3500        rawStopOrders = self.RequestStopOrders()
3501        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3502        lenSOrders = len(allStopOrdersIDs)
3503
3504        if lenOrders > 0 or lenSOrders > 0:
3505            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3506
3507            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3508
3509        else:
3510            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3512    def CloseAll(self, *args) -> None:
3513        """
3514        Close all available (not blocked) opened trades and orders.
3515
3516        Also, you can select one or more keywords case-insensitive:
3517        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3518
3519        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3520        """
3521        overview = self.Overview(show=False)  # get all open trades info
3522
3523        if len(args) == 0:
3524            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3525            self.CloseAllOrders()  # close all pending and stop orders
3526
3527            for iType in TKS_INSTRUMENTS:
3528                if iType != "Currencies":
3529                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3530
3531        else:
3532            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3533            lowerArgs = [x.lower() for x in args]
3534
3535            if "orders" in lowerArgs:
3536                self.CloseAllOrders()  # close all pending and stop orders
3537
3538            for iType in TKS_INSTRUMENTS:
3539                if iType.lower() in lowerArgs and iType != "Currencies":
3540                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3542    def CloseAllByTicker(self, instrument: str) -> None:
3543        """
3544        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3545
3546        This method searches opened trade and orders of instrument throw all portfolio and then use
3547        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3548
3549        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3550
3551        :param instrument: string with ticker.
3552        """
3553        if instrument is None or not instrument:
3554            uLogger.error("Ticker name must be defined for using this method!")
3555            raise Exception("Ticker required")
3556
3557        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3558
3559        self._ticker = instrument  # try to set instrument as ticker
3560        self._figi = ""
3561
3562        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3563        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3564
3565        if limitAll and self.IsInLimitOrders(portfolio=overview):
3566            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3567            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3568
3569        if stopAll and self.IsInStopOrders(portfolio=overview):
3570            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3571            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3572
3573        if self.IsInPortfolio(portfolio=overview):
3574            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3575            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3577    def CloseAllByFIGI(self, instrument: str) -> None:
3578        """
3579        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3580
3581        This method searches opened trade and orders of instrument throw all portfolio and then use
3582        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3583
3584        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3585
3586        :param instrument: string with FIGI id.
3587        """
3588        if instrument is None or not instrument:
3589            uLogger.error("FIGI id must be defined for using this method!")
3590            raise Exception("FIGI required")
3591
3592        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3593
3594        self._ticker = ""
3595        self._figi = instrument  # try to set instrument as FIGI id
3596
3597        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3598        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3599
3600        if limitAll and self.IsInLimitOrders(portfolio=overview):
3601            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3602            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3603
3604        if stopAll and self.IsInStopOrders(portfolio=overview):
3605            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3606            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3607
3608        if self.IsInPortfolio(portfolio=overview):
3609            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3610            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3612    @staticmethod
3613    def ParseOrderParameters(operation, **inputParameters):
3614        """
3615        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3616
3617        :param operation: string "Buy" or "Sell".
3618        :param inputParameters: this is dict of strings that looks like this
3619               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3620               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3621               "prices" key: one or more prices to open limit-orders
3622               Counts of values in lots and prices lists must be equals!
3623        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3624        """
3625        # TODO: update order grid work with api v2
3626        pass
3627        # uLogger.debug("Input parameters: {}".format(inputParameters))
3628        #
3629        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3630        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3631        #     raise Exception("Incorrect value")
3632        #
3633        # if "l" in inputParameters.keys():
3634        #     inputParameters["lots"] = inputParameters.pop("l")
3635        #
3636        # if "p" in inputParameters.keys():
3637        #     inputParameters["prices"] = inputParameters.pop("p")
3638        #
3639        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3640        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3641        #     raise Exception("Incorrect value")
3642        #
3643        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3644        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3645        #
3646        # if len(lots) != len(prices):
3647        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3648        #     raise Exception("Incorrect value")
3649        #
3650        # uLogger.debug("Extracted parameters for orders:")
3651        # uLogger.debug("lots = {}".format(lots))
3652        # uLogger.debug("prices = {}".format(prices))
3653        #
3654        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3655        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3656        # uLogger.debug("Order parameters: {}".format(result))
3657        #
3658        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3660    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3661        """
3662        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3663
3664        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3665        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3666        """
3667        result = False
3668        msg = "Instrument not defined!"
3669
3670        if portfolio is None or not portfolio:
3671            portfolio = self.Overview(show=False)
3672
3673        if self._ticker:
3674            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3675            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3676
3677            for iType in TKS_INSTRUMENTS:
3678                for instrument in portfolio["stat"][iType]:
3679                    if instrument["ticker"] == self._ticker:
3680                        result = True
3681                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3682                        break
3683
3684        elif self._figi:
3685            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3686            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3687
3688            for iType in TKS_INSTRUMENTS:
3689                for instrument in portfolio["stat"][iType]:
3690                    if instrument["figi"] == self._figi:
3691                        result = True
3692                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3693                        break
3694
3695        else:
3696            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3697
3698        uLogger.debug(msg)
3699
3700        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3702    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3703        """
3704        Returns instrument from the user's portfolio if it presents there.
3705        Instrument must be defined by `ticker` (highly priority) or `figi`.
3706
3707        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3708        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3709        """
3710        result = None
3711        msg = "Instrument not defined!"
3712
3713        if portfolio is None or not portfolio:
3714            portfolio = self.Overview(show=False)
3715
3716        if self._ticker:
3717            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3718            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3719
3720            for iType in TKS_INSTRUMENTS:
3721                for instrument in portfolio["stat"][iType]:
3722                    if instrument["ticker"] == self._ticker:
3723                        result = instrument
3724                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3725                        break
3726
3727        elif self._figi:
3728            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3729            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3730
3731            for iType in TKS_INSTRUMENTS:
3732                for instrument in portfolio["stat"][iType]:
3733                    if instrument["figi"] == self._figi:
3734                        result = instrument
3735                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3736                        break
3737
3738        else:
3739            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3740
3741        uLogger.debug(msg)
3742
3743        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3745    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3746        """
3747        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3748
3749        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3750
3751        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3752        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3753        """
3754        result = False
3755        msg = "Instrument not defined!"
3756
3757        if portfolio is None or not portfolio:
3758            portfolio = self.Overview(show=False)
3759
3760        if self._ticker:
3761            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3762            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3763
3764            for instrument in portfolio["stat"]["orders"]:
3765                if instrument["ticker"] == self._ticker:
3766                    result = True
3767                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3768                    break
3769
3770        elif self._figi:
3771            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3772            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3773
3774            for instrument in portfolio["stat"]["orders"]:
3775                if instrument["figi"] == self._figi:
3776                    result = True
3777                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3778                    break
3779
3780        else:
3781            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3782
3783        uLogger.debug(msg)
3784
3785        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3787    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3788        """
3789        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3790        Instrument must be defined by `ticker` (highly priority) or `figi`.
3791
3792        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3793
3794        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3795        :return: list with `orderID`s of limit orders.
3796        """
3797        result = []
3798        msg = "Instrument not defined!"
3799
3800        if portfolio is None or not portfolio:
3801            portfolio = self.Overview(show=False)
3802
3803        if self._ticker:
3804            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3805            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3806
3807            for instrument in portfolio["stat"]["orders"]:
3808                if instrument["ticker"] == self._ticker:
3809                    result.append(instrument["orderID"])
3810
3811            if result:
3812                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3813
3814        elif self._figi:
3815            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3816            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3817
3818            for instrument in portfolio["stat"]["orders"]:
3819                if instrument["figi"] == self._figi:
3820                    result.append(instrument["orderID"])
3821
3822            if result:
3823                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3824
3825        else:
3826            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3827
3828        uLogger.debug(msg)
3829
3830        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3832    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3833        """
3834        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3835
3836        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3837
3838        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3839        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3840        """
3841        result = False
3842        msg = "Instrument not defined!"
3843
3844        if portfolio is None or not portfolio:
3845            portfolio = self.Overview(show=False)
3846
3847        if self._ticker:
3848            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3849            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3850
3851            for instrument in portfolio["stat"]["stopOrders"]:
3852                if instrument["ticker"] == self._ticker:
3853                    result = True
3854                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3855                    break
3856
3857        elif self._figi:
3858            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3859            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3860
3861            for instrument in portfolio["stat"]["stopOrders"]:
3862                if instrument["figi"] == self._figi:
3863                    result = True
3864                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3865                    break
3866
3867        else:
3868            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3869
3870        uLogger.debug(msg)
3871
3872        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3874    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3875        """
3876        Returns list with all `orderID`s of opened stop orders for the instrument.
3877        Instrument must be defined by `ticker` (highly priority) or `figi`.
3878
3879        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3880
3881        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3882        :return: list with `orderID`s of stop orders.
3883        """
3884        result = []
3885        msg = "Instrument not defined!"
3886
3887        if portfolio is None or not portfolio:
3888            portfolio = self.Overview(show=False)
3889
3890        if self._ticker:
3891            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3892            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3893
3894            for instrument in portfolio["stat"]["stopOrders"]:
3895                if instrument["ticker"] == self._ticker:
3896                    result.append(instrument["orderID"])
3897
3898            if result:
3899                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3900
3901        elif self._figi:
3902            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3903            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3904
3905            for instrument in portfolio["stat"]["stopOrders"]:
3906                if instrument["figi"] == self._figi:
3907                    result.append(instrument["orderID"])
3908
3909            if result:
3910                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3911
3912        else:
3913            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3914
3915        uLogger.debug(msg)
3916
3917        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3919    def RequestLimits(self) -> dict:
3920        """
3921        Method for obtaining the available funds for withdrawal for current `accountId`.
3922
3923        See also:
3924        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3925        - `OverviewLimits()` method
3926
3927        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3928                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3929                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3930                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3931        """
3932        if self.accountId is None or not self.accountId:
3933            uLogger.error("Variable `accountId` must be defined for using this method!")
3934            raise Exception("Account ID required")
3935
3936        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3937
3938        self.body = str({"accountId": self.accountId})
3939        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3940        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3941
3942        if self.moreDebug:
3943            uLogger.debug("Records about available funds for withdrawal successfully received")
3944
3945        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3947    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3948        """
3949        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3950
3951        See also: `RequestLimits()`.
3952
3953        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3954        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3955        :return: dict with raw parsed data from server and some calculated statistics about it.
3956        """
3957        if self.accountId is None or not self.accountId:
3958            uLogger.error("Variable `accountId` must be defined for using this method!")
3959            raise Exception("Account ID required")
3960
3961        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3962
3963        view = {
3964            "rawLimits": rawLimits,
3965            "limits": {  # parsed data for every currency:
3966                "money": {  # this is an array of portfolio currency positions
3967                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3968                },
3969                "blocked": {  # this is an array of blocked currency
3970                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3971                },
3972                "blockedGuarantee": {  # this is locked money under collateral for futures
3973                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3974                },
3975            },
3976        }
3977
3978        # --- Prepare text table with limits in human-readable format:
3979        if show or onlyFiles:
3980            info = [
3981                "# Withdrawal limits\n\n",
3982                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3983                "* **Account ID:** [{}]\n".format(self.accountId),
3984            ]
3985
3986            if view["limits"]["money"]:
3987                info.extend([
3988                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3989                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3990                ])
3991
3992            else:
3993                info.append("\nNo withdrawal limits\n")
3994
3995            for curr in view["limits"]["money"].keys():
3996                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3997                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3998                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3999
4000                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4001                    "[{}]".format(curr),
4002                    "{:.2f}".format(view["limits"]["money"][curr]),
4003                    "{:.2f}".format(availableMoney),
4004                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4005                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4006                )
4007
4008                if curr == "rub":
4009                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4010
4011                else:
4012                    info.append(infoStr)
4013
4014            infoText = "".join(info)
4015
4016            if show and not onlyFiles:
4017                uLogger.info(infoText)
4018
4019            if self.withdrawalLimitsFile and (show or onlyFiles):
4020                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4021                    fH.write(infoText)
4022
4023                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4024
4025                if self.useHTMLReports:
4026                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4027                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4028                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4029
4030                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4031
4032        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
4034    def RequestAccounts(self) -> dict:
4035        """
4036        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4037
4038        See also:
4039        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4040        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4041        - `OverviewUserInfo()` method
4042
4043        :return: dict with raw data from server that contains accounts info. Example of dict:
4044                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4045                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4046                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4047                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4048        """
4049        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4050
4051        self.body = str({})
4052        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4053        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4054
4055        if self.moreDebug:
4056            uLogger.debug("Records about available accounts successfully received")
4057
4058        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4060    def RequestUserInfo(self) -> dict:
4061        """
4062        Method for requesting common user's information.
4063
4064        See also:
4065        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4066        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4067        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4068        - `OverviewUserInfo()` method
4069
4070        :return: dict with raw data from server that contains user's information. Example of dict:
4071                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4072                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4073        """
4074        uLogger.debug("Requesting common user's information. Wait, please...")
4075
4076        self.body = str({})
4077        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4078        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4079
4080        if self.moreDebug:
4081            uLogger.debug("Records about current user successfully received")
4082
4083        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4085    def RequestMarginStatus(self, accountId: str = None) -> dict:
4086        """
4087        Method for requesting margin calculation for defined account ID.
4088
4089        See also:
4090        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4091        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4092        - `OverviewUserInfo()` method
4093
4094        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4095        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4096                 Example of responses:
4097                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4098                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4099                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4100                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4101                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4102                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4103        """
4104        if accountId is None or not accountId:
4105            if self.accountId is None or not self.accountId:
4106                uLogger.error("Variable `accountId` must be defined for using this method!")
4107                raise Exception("Account ID required")
4108
4109            else:
4110                accountId = self.accountId  # use `self.accountId` (main ID) by default
4111
4112        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4113
4114        self.body = str({"accountId": accountId})
4115        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4116        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4117
4118        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4119            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4120            rawMargin = {}
4121
4122        else:
4123            if self.moreDebug:
4124                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4125
4126        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4128    def RequestTariffLimits(self) -> dict:
4129        """
4130        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4131
4132        See also:
4133        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4134        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4135        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4136        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4137        - `OverviewUserInfo()` method
4138
4139        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4140                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4141                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4142        """
4143        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4144
4145        self.body = str({})
4146        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4147        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4148
4149        if self.moreDebug:
4150            uLogger.debug("Records with limits of current tariff successfully received")
4151
4152        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4154    def RequestBondCoupons(self, iJSON: dict) -> dict:
4155        """
4156        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4157        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4158        All dates are in UTC timezone.
4159
4160        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4161        Documentation:
4162        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4163        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4164
4165        See also: `ExtendBondsData()`.
4166
4167        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4168                      If raw iJSON is not data of bond then server returns an error [400] with message:
4169                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4170        :return: dictionary with bond payment calendar. Response example
4171                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4172                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4173                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4174                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4175        """
4176        if iJSON["figi"] is None or not iJSON["figi"]:
4177            uLogger.error("FIGI must be defined for using this method!")
4178            raise Exception("FIGI required")
4179
4180        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4181        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4182
4183        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4184            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4185            self._figi,
4186            startDate,
4187            endDate,
4188        ))
4189
4190        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4191        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4192        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4193
4194        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4195            uLogger.warning("Instrument type is not bond!")
4196
4197        else:
4198            if self.moreDebug:
4199                uLogger.debug("Records about bond payment calendar successfully received")
4200
4201        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4203    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4204        """
4205        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4206        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4207        coupon yields, current yields and some statistics etc.
4208
4209        WARNING! This is too long operation if a lot of bonds requested from broker server.
4210
4211        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4212
4213        :param instruments: list of strings with tickers or FIGIs.
4214        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4215                     for further used by data scientists or stock analytics.
4216        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4217                 In XLSX-file and Pandas DataFrame fields mean:
4218                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4219                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4220        """
4221        if instruments is None or not instruments:
4222            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4223            raise Exception("Ticker or FIGI required")
4224
4225        if isinstance(instruments, str):
4226            instruments = [instruments]
4227
4228        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4229
4230        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4231
4232        iCount = len(uniqueInstruments)
4233        tooLong = iCount >= 20
4234        if tooLong:
4235            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4236
4237        bonds = None
4238        for i, self._figi in enumerate(uniqueInstruments):
4239            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4240
4241            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4242                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4243                rawBond = self.SearchByFIGI(requestPrice=True)
4244
4245                # Widen raw data with UTC current time (iData["actualDateTime"]):
4246                actualDate = datetime.now(tzutc())
4247                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4248
4249                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4250                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4251
4252                # Replace some values with human-readable:
4253                iData["nominalCurrency"] = iData["nominal"]["currency"]
4254                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4255                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4256                iData["aciCurrency"] = iData["aciValue"]["currency"]
4257                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4258                iData["issueSize"] = int(iData["issueSize"])
4259                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4260                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4261                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4262                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4263                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4264                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4265                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4266                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4267                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4268                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4269
4270                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4271                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4272                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4273                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4274                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4275                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4276                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4277                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4278                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4279                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4280                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4281
4282                # Widen raw data with calendar data from `rawCalendar` values:
4283                calendarData = []
4284                if "events" in iData["rawCalendar"].keys():
4285                    for item in iData["rawCalendar"]["events"]:
4286                        calendarData.append({
4287                            "couponDate": item["couponDate"],
4288                            "couponNumber": int(item["couponNumber"]),
4289                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4290                            "payCurrency": item["payOneBond"]["currency"],
4291                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4292                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4293                            "couponStartDate": item["couponStartDate"],
4294                            "couponEndDate": item["couponEndDate"],
4295                            "couponPeriod": item["couponPeriod"],
4296                        })
4297
4298                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4299                    if "maturityDate" not in iData.keys():
4300                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4301
4302                # Widen raw data with Coupon Rate.
4303                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4304                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4305                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4306                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4307
4308                # Widen raw data with Yield to Maturity (YTM) on current date.
4309                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4310                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4311                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4312                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4313                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4314                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4315
4316                iData["calendar"] = calendarData  # adds calendar at the end
4317
4318                # Remove not used data:
4319                iData.pop("uid")
4320                iData.pop("positionUid")
4321                iData.pop("currentPrice")
4322                iData.pop("rawCalendar")
4323
4324                colNames = list(iData.keys())
4325                if bonds is None:
4326                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4327
4328                else:
4329                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4330
4331            else:
4332                uLogger.warning("Instrument is not a bond!")
4333
4334            processed = round(100 * (i + 1) / iCount, 1)
4335            if tooLong and processed % 5 == 0:
4336                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4337
4338            else:
4339                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4340
4341        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4342
4343        # Saving bonds from Pandas DataFrame to XLSX sheet:
4344        if xlsx and self.bondsXLSXFile:
4345            with pd.ExcelWriter(
4346                    path=self.bondsXLSXFile,
4347                    date_format=TKS_DATE_FORMAT,
4348                    datetime_format=TKS_DATE_TIME_FORMAT,
4349                    mode="w",
4350            ) as writer:
4351                bonds.to_excel(
4352                    writer,
4353                    sheet_name="Extended bonds data",
4354                    index=True,
4355                    encoding="UTF-8",
4356                    freeze_panes=(1, 1),
4357                )  # saving as XLSX-file with freeze first row and column as headers
4358
4359            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4360
4361        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4363    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4364        """
4365        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4366
4367        WARNING! This is too long operation if a lot of bonds requested from broker server.
4368
4369        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4370
4371        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4372                        extended information about bonds: main info, current prices, bond payment calendar,
4373                        coupon yields, current yields and some statistics etc.
4374                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4375        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4376                     for further used by data scientists or stock analytics.
4377        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4378        """
4379        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4380            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4381
4382        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4383
4384        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4385        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4386        calendar = None
4387        for bond in extBonds.iterrows():
4388            for item in bond[1]["calendar"]:
4389                cData = {
4390                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4391                    "couponDate": item["couponDate"],
4392                    "figi": bond[1]["figi"],
4393                    "ticker": bond[1]["ticker"],
4394                    "name": bond[1]["name"],
4395                    "couponNumber": item["couponNumber"],
4396                    "payOneBond": item["payOneBond"],
4397                    "payCurrency": item["payCurrency"],
4398                    "couponType": item["couponType"],
4399                    "couponPeriod": item["couponPeriod"],
4400                    "fixDate": item["fixDate"],
4401                    "couponStartDate": item["couponStartDate"],
4402                    "couponEndDate": item["couponEndDate"],
4403                }
4404
4405                if calendar is None:
4406                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4407
4408                else:
4409                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4410
4411        if calendar is not None:
4412            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4413
4414            # Saving calendar from Pandas DataFrame to XLSX sheet:
4415            if xlsx:
4416                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4417
4418                with pd.ExcelWriter(
4419                        path=xlsxCalendarFile,
4420                        date_format=TKS_DATE_FORMAT,
4421                        datetime_format=TKS_DATE_TIME_FORMAT,
4422                        mode="w",
4423                ) as writer:
4424                    humanReadable = calendar.copy(deep=True)
4425                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4426                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4427                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4428                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4429                    humanReadable.columns = colNames  # human-readable column names
4430
4431                    humanReadable.to_excel(
4432                        writer,
4433                        sheet_name="Bond payments calendar",
4434                        index=False,
4435                        encoding="UTF-8",
4436                        freeze_panes=(1, 2),
4437                    )  # saving as XLSX-file with freeze first row and column as headers
4438
4439                    del humanReadable  # release df in memory
4440
4441                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4442
4443        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, show: bool = True, onlyFiles=False) -> str:
4445    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4446        """
4447        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4448        Also, creates Markdown file with calendar data, `calendar.md` by default.
4449
4450        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4451
4452        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4453                        extended information about bonds: main info, current prices, bond payment calendar,
4454                        coupon yields, current yields and some statistics etc.
4455                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4456        :param show: if `True` then also printing bonds payment calendar to the console,
4457                     otherwise save to file `calendarFile` only. `False` by default.
4458        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4459        :return: multilines text in Markdown format with bonds payment calendar as a table.
4460        """
4461        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4462            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4463
4464        infoText = "# Bond payments calendar\n\n"
4465
4466        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4467
4468        if not (calendar is None or calendar.empty):
4469            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4470
4471            info = [
4472                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4473                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4474                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4475            ]
4476
4477            newMonth = False
4478            notOneBond = calendar["figi"].nunique() > 1
4479            for i, bond in enumerate(calendar.iterrows()):
4480                if newMonth and notOneBond:
4481                    info.append(splitLine)
4482
4483                info.append(
4484                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4485                        "  √" if bond[1]["paid"] else "  —",
4486                        bond[1]["couponDate"].split("T")[0],
4487                        bond[1]["figi"],
4488                        bond[1]["ticker"],
4489                        bond[1]["couponNumber"],
4490                        "{} {}".format(
4491                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4492                            bond[1]["payCurrency"],
4493                        ),
4494                        bond[1]["couponType"],
4495                        bond[1]["couponPeriod"],
4496                        bond[1]["fixDate"].split("T")[0],
4497                    )
4498                )
4499
4500                if i < len(calendar.values) - 1:
4501                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4502                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4503                    newMonth = False if curDate.month == nextDate.month else True
4504
4505                else:
4506                    newMonth = False
4507
4508            infoText += "".join(info)
4509
4510            if show and not onlyFiles:
4511                uLogger.info("{}".format(infoText))
4512
4513            if self.calendarFile is not None and (show or onlyFiles):
4514                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4515                    fH.write(infoText)
4516
4517                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4518
4519                if self.useHTMLReports:
4520                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4521                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4522                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4523
4524                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4525
4526        else:
4527            infoText += "No data\n"
4528
4529        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4531    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4532        """
4533        Method for parsing and show simple table with all available user accounts.
4534
4535        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4536
4537        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4538        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4539        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4540                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4541                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4542                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4543                                                        "closed": "—", "access": "Full access" }, ...}}`
4544        """
4545        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4546
4547        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4548        accounts = {
4549            item["id"]: {
4550                "type": TKS_ACCOUNT_TYPES[item["type"]],
4551                "name": item["name"],
4552                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4553                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4554                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4555                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4556            } for item in rawAccounts["accounts"]
4557        }
4558
4559        # Raw and parsed data with some fields replaced in "stat" section:
4560        view = {
4561            "rawAccounts": rawAccounts,
4562            "stat": accounts,
4563        }
4564
4565        # --- Prepare simple text table with only accounts data in human-readable format:
4566        if show or onlyFiles:
4567            info = [
4568                "# User accounts\n\n",
4569                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4570                "| Account ID   | Type                      | Status                    | Name                           |\n",
4571                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4572            ]
4573
4574            for account in view["stat"].keys():
4575                info.extend([
4576                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4577                        account,
4578                        view["stat"][account]["type"],
4579                        view["stat"][account]["status"],
4580                        view["stat"][account]["name"],
4581                    )
4582                ])
4583
4584            infoText = "".join(info)
4585
4586            if show and not onlyFiles:
4587                uLogger.info(infoText)
4588
4589            if self.userAccountsFile and (show or onlyFiles):
4590                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4591                    fH.write(infoText)
4592
4593                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4594
4595                if self.useHTMLReports:
4596                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4597                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4598                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4599
4600                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4601
4602        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4604    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4605        """
4606        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4607
4608        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4609
4610        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4611        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4612        :return: dict with raw parsed data from server and some calculated statistics about it.
4613        """
4614        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4615        tmpTicker = self._ticker
4616        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4617        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4618        self._ticker = tmpTicker
4619
4620        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4621        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4622        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4623        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4624        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4625        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4626
4627        # This is dict with parsed common user data:
4628        userInfo = {
4629            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4630            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4631            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4632            "tariff": rawUserInfo["tariff"],
4633        }
4634
4635        # This is an array of dict with parsed margin statuses for every account IDs:
4636        margins = {}
4637        for accountId in accounts.keys():
4638            if rawMargins[accountId]:
4639                margins[accountId] = {
4640                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4641                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4642                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4643                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4644                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4645                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4646                    "missing": missing["volume"],
4647                }
4648
4649            else:
4650                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4651
4652        unary = {}  # unary-connection limits
4653        for item in rawTariffLimits["unaryLimits"]:
4654            if item["limitPerMinute"] in unary.keys():
4655                unary[item["limitPerMinute"]].extend(item["methods"])
4656
4657            else:
4658                unary[item["limitPerMinute"]] = item["methods"]
4659
4660        stream = {}  # stream-connection limits
4661        for item in rawTariffLimits["streamLimits"]:
4662            if item["limit"] in stream.keys():
4663                stream[item["limit"]].extend(item["streams"])
4664
4665            else:
4666                stream[item["limit"]] = item["streams"]
4667
4668        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4669        limits = {
4670            "unary": unary,
4671            "stream": stream,
4672        }
4673
4674        # Raw and parsed data as an output result:
4675        view = {
4676            "rawUserInfo": rawUserInfo,
4677            "rawAccounts": rawAccounts,
4678            "rawMargins": rawMargins,
4679            "rawTariffLimits": rawTariffLimits,
4680            "stat": {
4681                "overview": overview,
4682                "userInfo": userInfo,
4683                "accounts": accounts,
4684                "margins": margins,
4685                "limits": limits,
4686            },
4687        }
4688
4689        # --- Prepare text table with user information in human-readable format:
4690        if show or onlyFiles:
4691            info = [
4692                "# Full user information\n\n",
4693                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4694                "## Common information\n\n",
4695                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4696                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4697                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4698                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4699                "\n## User accounts\n\n",
4700            ]
4701
4702            for account in view["stat"]["accounts"].keys():
4703                info.extend([
4704                    "### ID: [{}]\n\n".format(account),
4705                    "| Parameters           | Values                                                       |\n",
4706                    "|----------------------|--------------------------------------------------------------|\n",
4707                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4708                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4709                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4710                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4711                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4712                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4713                ])
4714
4715                if margins[account]:
4716                    info.extend([
4717                        "| Margin status:       | Enabled                                                      |\n",
4718                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4719                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4720                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4721                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4722                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4723                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4724                    ])
4725
4726                else:
4727                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4728
4729            info.extend([
4730                "\n## Current user tariff limits\n",
4731                "\n### See also\n",
4732                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4733                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4734                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4735                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4736                "\n### Unary limits\n",
4737            ])
4738
4739            if unary:
4740                for key, values in sorted(unary.items()):
4741                    info.append("\n* Max requests per minute: {}\n".format(key))
4742
4743                    for value in values:
4744                        info.append("  - {}\n".format(value))
4745
4746            else:
4747                info.append("\nNot available\n")
4748
4749            info.append("\n### Stream limits\n")
4750
4751            if stream:
4752                for key, values in sorted(stream.items()):
4753                    info.append("\n* Max stream connections: {}\n".format(key))
4754
4755                    for value in values:
4756                        info.append("  - {}\n".format(value))
4757
4758            else:
4759                info.append("\nNot available\n")
4760
4761            infoText = "".join(info)
4762
4763            if show and not onlyFiles:
4764                uLogger.info(infoText)
4765
4766            if self.userInfoFile and (show or onlyFiles):
4767                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4768                    fH.write(infoText)
4769
4770                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4771
4772                if self.useHTMLReports:
4773                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4774                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4775                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4776
4777                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4778
4779        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4782class Args:
4783    """
4784    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4785    """
4786    def __init__(self, **kwargs):
4787        self.__dict__.update(kwargs)
4788
4789    def __getattr__(self, item):
4790        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4786    def __init__(self, **kwargs):
4787        self.__dict__.update(kwargs)
def ParseArgs():
4793def ParseArgs():
4794    """This function get and parse command line keys."""
4795    parser = ArgumentParser()  # command-line string parser
4796
4797    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4798    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4799
4800    # --- options:
4801
4802    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4803    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4804    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4805
4806    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4807    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4808
4809    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4810    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4811
4812    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4813    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4814
4815    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4816    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4817    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4818
4819    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4820    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4821    parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).")
4822
4823    # --- commands:
4824
4825    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4826
4827    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4828    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4829    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4830    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4831    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4832    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4833    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4834    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4835
4836    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4837    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4838    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4839    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4840    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4841    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4842
4843    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4844    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4845    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4846    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4847
4848    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4849    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4850    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4851
4852    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4853    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4854    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4855    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4856    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4857    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4858    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4859
4860    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4861    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4862    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4863    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4864    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4865
4866    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4867    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4868    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4869
4870    cmdArgs = parser.parse_args()
4871    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4874def Main(**kwargs):
4875    """
4876    Main function for work with TKSBrokerAPI in the console.
4877
4878    See examples:
4879    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4880    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4881    """
4882    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4883
4884    if args.debug_level:
4885        uLogger.level = 10  # always debug level by default
4886        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4887
4888    exitCode = 0
4889    start = datetime.now(tzutc())
4890    uLogger.debug("=-" * 50)
4891    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4892        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4893        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4894    ))
4895
4896    # trying to calculate full current version:
4897    buildVersion = __version__
4898    try:
4899        v = version("tksbrokerapi")
4900        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4901
4902    except Exception:
4903        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4904
4905    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4906    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4907
4908    try:
4909        if args.version:
4910            print("TKSBrokerAPI {}".format(buildVersion))
4911            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4912
4913        else:
4914            # Init class for trading with Tinkoff Broker:
4915            trader = TinkoffBrokerServer(
4916                token=args.token,
4917                accountId=args.account_id,
4918                useCache=not args.no_cache,
4919            )
4920
4921            if args.tag is not None:
4922                trader.tag = args.tag  # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode
4923
4924            # --- set some options:
4925
4926            if args.more:
4927                trader.moreDebug = True
4928                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4929
4930            if args.html:
4931                trader.useHTMLReports = True
4932
4933            if args.ticker:
4934                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4935
4936                if ticker in trader.aliasesKeys:
4937                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4938
4939                else:
4940                    trader.ticker = ticker
4941
4942            if args.figi:
4943                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4944
4945            if args.depth is not None:
4946                trader.depth = args.depth
4947
4948            # --- do one command:
4949
4950            if args.list:
4951                if args.output is not None:
4952                    trader.instrumentsFile = args.output
4953
4954                trader.ShowInstrumentsInfo(show=True)
4955
4956            elif args.list_xlsx:
4957                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4958
4959            elif args.bonds_xlsx is not None:
4960                if args.output is not None:
4961                    trader.bondsXLSXFile = args.output
4962
4963                if len(args.bonds_xlsx) == 0:
4964                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4965
4966                else:
4967                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4968
4969            elif args.search:
4970                if args.output is not None:
4971                    trader.searchResultsFile = args.output
4972
4973                trader.SearchInstruments(pattern=args.search[0], show=True)
4974
4975            elif args.info:
4976                if not (args.ticker or args.figi):
4977                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4978                    raise Exception("Ticker or FIGI required")
4979
4980                if args.output is not None:
4981                    trader.infoFile = args.output
4982
4983                if args.ticker:
4984                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4985
4986                else:
4987                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4988
4989            elif args.calendar is not None:
4990                if args.output is not None:
4991                    trader.calendarFile = args.output
4992
4993                if len(args.calendar) == 0:
4994                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4995
4996                else:
4997                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4998
4999                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
5000
5001            elif args.price:
5002                if not (args.ticker or args.figi):
5003                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5004                    raise Exception("Ticker or FIGI required")
5005
5006                trader.GetCurrentPrices(show=True)
5007
5008            elif args.prices is not None:
5009                if args.output is not None:
5010                    trader.pricesFile = args.output
5011
5012                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
5013
5014            elif args.overview:
5015                if args.output is not None:
5016                    trader.overviewFile = args.output
5017
5018                trader.Overview(show=True, details="full")
5019
5020            elif args.overview_digest:
5021                if args.output is not None:
5022                    trader.overviewDigestFile = args.output
5023
5024                trader.Overview(show=True, details="digest")
5025
5026            elif args.overview_positions:
5027                if args.output is not None:
5028                    trader.overviewPositionsFile = args.output
5029
5030                trader.Overview(show=True, details="positions")
5031
5032            elif args.overview_orders:
5033                if args.output is not None:
5034                    trader.overviewOrdersFile = args.output
5035
5036                trader.Overview(show=True, details="orders")
5037
5038            elif args.overview_analytics:
5039                if args.output is not None:
5040                    trader.overviewAnalyticsFile = args.output
5041
5042                trader.Overview(show=True, details="analytics")
5043
5044            elif args.overview_calendar:
5045                if args.output is not None:
5046                    trader.overviewAnalyticsFile = args.output
5047
5048                trader.Overview(show=True, details="calendar")
5049
5050            elif args.deals is not None:
5051                if args.output is not None:
5052                    trader.reportFile = args.output
5053
5054                if 0 <= len(args.deals) < 3:
5055                    trader.Deals(
5056                        start=args.deals[0] if len(args.deals) >= 1 else None,
5057                        end=args.deals[1] if len(args.deals) == 2 else None,
5058                        show=True,  # Always show deals report in console
5059                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5060                    )
5061
5062                else:
5063                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5064                    raise Exception("Incorrect value")
5065
5066            elif args.history is not None:
5067                if args.output is not None:
5068                    trader.historyFile = args.output
5069
5070                if 0 <= len(args.history) < 3:
5071                    dataReceived = trader.History(
5072                        start=args.history[0] if len(args.history) >= 1 else None,
5073                        end=args.history[1] if len(args.history) == 2 else None,
5074                        interval="hour" if args.interval is None or not args.interval else args.interval,
5075                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5076                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5077                        show=True,  # shows all downloaded candles in console
5078                    )
5079
5080                    if args.render_chart is not None and dataReceived is not None:
5081                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5082
5083                        trader.ShowHistoryChart(
5084                            candles=dataReceived,
5085                            interact=iChart,
5086                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5087                        )
5088
5089                else:
5090                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5091                    raise Exception("Incorrect value")
5092
5093            elif args.load_history is not None:
5094                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5095
5096                if args.render_chart is not None and histData is not None:
5097                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5098                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5099
5100                    trader.ShowHistoryChart(
5101                        candles=histData,
5102                        interact=iChart,
5103                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5104                    )
5105
5106            elif args.trade is not None:
5107                if 1 <= len(args.trade) <= 5:
5108                    trader.Trade(
5109                        operation=args.trade[0],
5110                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5111                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5112                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5113                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5114                    )
5115
5116                else:
5117                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5118
5119            elif args.buy is not None:
5120                if 0 <= len(args.buy) <= 4:
5121                    trader.Buy(
5122                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5123                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5124                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5125                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5126                    )
5127
5128                else:
5129                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5130
5131            elif args.sell is not None:
5132                if 0 <= len(args.sell) <= 4:
5133                    trader.Sell(
5134                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5135                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5136                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5137                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5138                    )
5139
5140                else:
5141                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5142
5143            elif args.order:
5144                if 4 <= len(args.order) <= 7:
5145                    trader.Order(
5146                        operation=args.order[0],
5147                        orderType=args.order[1],
5148                        lots=int(args.order[2]),
5149                        targetPrice=float(args.order[3]),
5150                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5151                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5152                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5153                    )
5154
5155                else:
5156                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5157
5158            elif args.buy_limit:
5159                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5160
5161            elif args.sell_limit:
5162                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5163
5164            elif args.buy_stop:
5165                if 2 <= len(args.buy_stop) <= 7:
5166                    trader.BuyStop(
5167                        lots=int(args.buy_stop[0]),
5168                        targetPrice=float(args.buy_stop[1]),
5169                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5170                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5171                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5172                    )
5173
5174                else:
5175                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5176
5177            elif args.sell_stop:
5178                if 2 <= len(args.sell_stop) <= 7:
5179                    trader.SellStop(
5180                        lots=int(args.sell_stop[0]),
5181                        targetPrice=float(args.sell_stop[1]),
5182                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5183                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5184                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5185                    )
5186
5187                else:
5188                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5189
5190            # elif args.buy_order_grid is not None:
5191            #     # update order grid work with api v2
5192            #     if len(args.buy_order_grid) == 2:
5193            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5194            #
5195            #         for order in orderParams:
5196            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5197            #
5198            #     else:
5199            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5200            #
5201            # elif args.sell_order_grid is not None:
5202            #     # update order grid work with api v2
5203            #     if len(args.sell_order_grid) >= 2:
5204            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5205            #
5206            #         for order in orderParams:
5207            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5208            #
5209            #     else:
5210            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5211
5212            elif args.close_order is not None:
5213                trader.CloseOrders(args.close_order)  # close only one order
5214
5215            elif args.close_orders is not None:
5216                trader.CloseOrders(args.close_orders)  # close list of orders
5217
5218            elif args.close_trade:
5219                if not (args.ticker or args.figi):
5220                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5221                    raise Exception("Ticker or FIGI required")
5222
5223                if args.ticker:
5224                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5225
5226                else:
5227                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5228
5229            elif args.close_trades is not None:
5230                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5231
5232            elif args.close_all is not None:
5233                if args.ticker:
5234                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5235
5236                elif args.figi:
5237                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5238
5239                else:
5240                    trader.CloseAll(*args.close_all)
5241
5242            elif args.limits:
5243                if args.output is not None:
5244                    trader.withdrawalLimitsFile = args.output
5245
5246                trader.OverviewLimits(show=True)
5247
5248            elif args.user_info:
5249                if args.output is not None:
5250                    trader.userInfoFile = args.output
5251
5252                trader.OverviewUserInfo(show=True)
5253
5254            elif args.account:
5255                if args.output is not None:
5256                    trader.userAccountsFile = args.output
5257
5258                trader.OverviewAccounts(show=True)
5259
5260            else:
5261                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5262                raise Exception("There is no command to execute")
5263
5264    except Exception:
5265        trace = tb.format_exc()
5266        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5267            if e in trace:
5268                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5269                break
5270
5271        uLogger.debug(trace)
5272        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5273        exitCode = 255  # an error occurred, must be open a ticket for this issue
5274
5275    finally:
5276        finish = datetime.now(tzutc())
5277
5278        if exitCode == 0:
5279            if args.more:
5280                uLogger.debug("All operations were finished success (summary code is 0).")
5281
5282        else:
5283            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5284                os.path.abspath(uLog.defaultLogFile), exitCode,
5285            ))
5286
5287        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5288        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5289            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5290            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5291        ))
5292        uLogger.debug("=-" * 50)
5293
5294        if not kwargs:
5295            sys.exit(exitCode)
5296
5297        else:
5298            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: